feat(solution): 발행 워커·버전 관리와 예약·미리보기 정리

상시 프리렌더와 중복 예약 안내를 없애고, 검수된 발행 버전을 보존한다. 미리보기는 실제 렌더 완료까지 스피너를 표시한다.

사이트 81건, 발행·롤백·서치콘솔 45건, 프로세스 수명 3건 통과. 빌더·사이트 빌드 및 compose 설정 검증 통과.
This commit is contained in:
Mina Choi 2026-09-15 16:10:12 +09:00
parent 0f7d22750f
commit f409c11624
204 changed files with 8383 additions and 4427 deletions

View File

@ -1,5 +1,8 @@
# AGENTS.md — 이 레포에서 작업하기 전에 # AGENTS.md — 이 레포에서 작업하기 전에
> 2026-09-15 발행 버전 전환: [docs/PUBLISH_VERSION.md](docs/PUBLISH_VERSION.md)가
> 아래의 상시 프리렌더·재굽기·자산 주소 교체 절차를 대체한다. 목업은 변경하지 않는다.
에이전트와 신규 합류자가 **먼저 읽는 파일**이다. 여기에는 *밟기 쉬운 함정*과 *규약*만 둔다. 에이전트와 신규 합류자가 **먼저 읽는 파일**이다. 여기에는 *밟기 쉬운 함정*과 *규약*만 둔다.
설명은 각 문서가 단일 출처다 — 여기로 복사하지 말고 링크한다. 설명은 각 문서가 단일 출처다 — 여기로 복사하지 말고 링크한다.

141
deploy.sh
View File

@ -1,127 +1,40 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# 배포 — 코드를 당기고, 지정한 서비스만 다시 빌드해 갈아끼운다. # 서버 변경과 꺼진 admin을 보존한다. 렌더러 배포도 기존 사이트를 재굽지 않는다.
#
# ./deploy.sh 전체
# ./deploy.sh solution-backend 그 서비스만
# ./deploy.sh solution-backend solution-worker 여럿
#
# 서비스명 대신 컨테이너명(o2o-web4ai-solution-backend)으로 불러도 받는다.
set -euo pipefail set -euo pipefail
cd "$(dirname "$0")" cd "$(dirname "$0")"
PREFIX=o2o-web4ai
# api·worker·api-admin 은 이미지 한 벌(o2o-web4ai-backend)을 나눠 쓴다.
BACKEND_SVCS=(solution-backend solution-worker admin-backend)
BRANCH=${DEPLOY_BRANCH:-main}
PULL=1 PULL=1
ONLY=0 ONLY=0
TARGETS=() TARGETS=()
for arg in "$@"; do
usage() { case "$arg" in
cat <<'USAGE'
사용법: ./deploy.sh [옵션] [서비스...]
옵션
--no-pull 코드를 당기지 않는다(디스크에 있는 코드 그대로 빌드)
--only 백엔드 형제 서비스를 함께 갈아끼우지 않는다 (아래 ★ 참고)
-h, --help
환경변수
DEPLOY_BRANCH 기본 main. 다른 브랜치를 배포할 때만 쓴다
서비스: solution-backend · solution-worker · solution-prerender · solution-site
admin-backend · admin-frontend (프로필 admin, 기본 기동에서 빠져 있다)
solution-frontend (프로필 dev, 로컬 HMR 전용 — 운영에서 띄우지 않는다)
o2o-web4ai-solution-backend 처럼 컨테이너명으로 적어도 된다
★ solution-backend·solution-worker·admin-backend 는 이미지가 한 벌이다. 하나를 빌드하면 나머지도 새 이미지로
갈아끼워야 한다 — 안 그러면 옛 코드로 도는 컨테이너가 남는데, 셋 다 "살아 있음" 이라
화면상으로는 배포가 끝난 것처럼 보인다. --only 는 그걸 알고 건너뛸 때만 쓴다.
USAGE
}
while [ $# -gt 0 ]; do
case "$1" in
--no-pull) PULL=0 ;; --no-pull) PULL=0 ;;
--only) ONLY=1 ;; --only) ONLY=1 ;;
-h|--help) usage; exit 0 ;; -h|--help) echo "사용법: ./deploy.sh [--no-pull] [--only] [서비스...]"; exit 0 ;;
-*) echo "모르는 옵션: $1" >&2; usage >&2; exit 2 ;; -*) echo "모르는 옵션: $arg" >&2; exit 2 ;;
*) TARGETS+=("${1#"$PREFIX"-}") ;; # 컨테이너명으로 불러도 받는다 *) TARGETS+=("${arg#o2o-web4ai-}") ;;
esac esac
shift
done done
ALL_SVCS=$(docker compose config --services)
for t in ${TARGETS+"${TARGETS[@]}"}; do
grep -qx "$t" <<<"$ALL_SVCS" || {
echo "그런 서비스가 없다: $t" >&2
echo "있는 것: $(tr '\n' ' ' <<<"$ALL_SVCS")" >&2
exit 2
}
done
# ── 코드 ────────────────────────────────────────────────────────────
# 배포 서버의 작업트리는 main 의 **사본**이지 작업 공간이 아니다. 그래서 머지(pull)가 아니라
# origin/$BRANCH 로 하드 리셋한다 — 밖에서 force-push 가 나도 --ff-only 로 막히지 않고,
# 서버에서 누가 손댄 흔적이 다음 배포까지 살아남지 않는다.
#
# ★ fetch 가 실패하면 **리셋하지 않는다.** 이 서버엔 gitea 자격증명이 없어 fetch 가 죽는데,
# 그 상태의 origin/$BRANCH 는 마지막으로 fetch 된 낡은 ref 다.
# 실측(2026-09-01 킹서버): HEAD=9b4fe40 인데 origin/main=4871e50 — 믿고 리셋하면 한 커밋
# 롤백된다. 그러고도 빌드는 성공하고 컨테이너는 뜬다. 조용히 틀리는 종류다.
#
# ★ git clean 은 하지 않는다. .env 와 nginx/site.conf 는 추적되지 않는 파일이고 서버마다
# 다르다 — reset --hard 는 이 둘을 건드리지 않지만 clean 은 지운다.
if [ "$PULL" = 1 ]; then if [ "$PULL" = 1 ]; then
echo "▶ git fetch origin $BRANCH" git diff --quiet && git diff --cached --quiet || { echo "서버 변경을 먼저 정리하세요" >&2; exit 1; }
if git fetch --prune origin "$BRANCH" 2>&1; then git pull --ff-only origin "${DEPLOY_BRANCH:-main}"
DIRTY=$(git status --porcelain)
if [ -n "$DIRTY" ]; then
echo " ! 작업트리 변경을 버린다:"
sed 's/^/ /' <<<"$DIRTY"
fi
echo "▶ git reset --hard origin/$BRANCH"
git reset --hard "origin/$BRANCH"
else
echo " ! fetch 실패 — 리셋을 건너뛰고 디스크에 있는 코드 그대로 간다." >&2
echo " origin/$BRANCH 가 낡았을 수 있어 그걸로 리셋하면 배포가 조용히 롤백된다." >&2
echo " (밖에서 밀어넣었으면 이대로 두면 되고, 자동화하려면 gitea 배포키를 건다)" >&2
fi
fi fi
echo "▶ 지금 코드: $(git log --oneline -1)" [ ${#TARGETS[@]} -gt 0 ] || TARGETS=(solution-backend solution-worker solution-site)
RUNNING=$(docker compose ps --services --status running)
# ── 대상 정하기 ───────────────────────────────────────────────────── if [ "$ONLY" = 0 ]; then
if [ ${#TARGETS[@]} -eq 0 ]; then case " ${TARGETS[*]} " in
SVCS=() # 빈 인자 = compose 가 전부로 해석한다 *" solution-backend "*|*" solution-worker "*)
echo "▶ 대상: 전체" TARGETS+=(solution-backend solution-worker)
else if grep -qx admin-backend <<<"$RUNNING"; then TARGETS+=(admin-backend); fi ;;
SVCS=("${TARGETS[@]}") esac
# 백엔드 하나를 건드리면 같은 이미지를 쓰는 형제도 함께 갈아끼운다.
if [ "$ONLY" = 0 ]; then
for t in "${TARGETS[@]}"; do
for b in "${BACKEND_SVCS[@]}"; do [ "$t" = "$b" ] || continue
for sib in "${BACKEND_SVCS[@]}"; do
printf '%s\n' "${SVCS[@]}" | grep -qx "$sib" || {
SVCS+=("$sib")
echo " + $sib — 같은 이미지를 쓴다(옛 코드로 남지 않게 함께 간다)"
}
done
done
done
fi
echo "▶ 대상: ${SVCS[*]}"
fi fi
SERVICES=()
# ── 빌드 · 교체 ───────────────────────────────────────────────────── for target in "${TARGETS[@]}"; do
# build 절이 없는 서비스(node:24-alpine · nginx:alpine)는 build 가 조용히 건너뛴다. case " ${SERVICES[*]-} " in *" $target "*) ;; *) SERVICES+=("$target");; esac
echo "▶ build" done
docker compose build ${SVCS+"${SVCS[@]}"} # site의 렌더러도 worker 이미지에 들어간다.
case " ${SERVICES[*]} " in
echo "▶ up -d" *" solution-site "*) case " ${SERVICES[*]} " in *" solution-worker "*) ;; *) SERVICES+=(solution-worker);; esac ;;
docker compose up -d --force-recreate ${SVCS+"${SVCS[@]}"} esac
docker compose build "${SERVICES[@]}"
echo docker compose up -d --no-deps --force-recreate "${SERVICES[@]}"
docker compose ps docker compose ps
echo
echo "로그: ./log.sh"

View File

@ -28,8 +28,12 @@ x-common-env: &common-env
# ★ 기본값은 localhost 다. 운영 도메인을 기본으로 두면 .env 를 안 채운 로컬 빌드가 # ★ 기본값은 localhost 다. 운영 도메인을 기본으로 두면 .env 를 안 채운 로컬 빌드가
# 조용히 운영 주소를 번들에 굽는다(실측 2026-09-03: 로컬 링크가 킹서버로 갔다). # 조용히 운영 주소를 번들에 굽는다(실측 2026-09-03: 로컬 링크가 킹서버로 갔다).
SITE_PUBLIC_HOST: ${SITE_PUBLIC_HOST:-localhost} SITE_PUBLIC_HOST: ${SITE_PUBLIC_HOST:-localhost}
SITE_PAYLOAD_DIR: /app/out/payloads # ★ 렌더러(solution/site/scripts/prerender.ts)가 songs·out 디렉토리를 **자기 파일 위치
SITE_OUTPUT_DIR: /app/out/sites # 기준 상대경로**로 찾는다(SITE_ROOT = dist/prerender/../..) — 워커 컨테이너 안에서 그
# 렌더러를 직접 띄우므로(render_service.py) 세 디렉토리가 실제 설치 자리
# (/app/solution/site/) 아래 나란히 있어야 한다. 아래 볼륨 마운트와 짝이 맞아야 한다.
SITE_PAYLOAD_DIR: /app/solution/site/payloads
SITE_OUTPUT_DIR: /app/solution/site/out
# ★ 프리렌더와 같은 값이어야 한다. 어긋나면 색인 통보가 403 이다. # ★ 프리렌더와 같은 값이어야 한다. 어긋나면 색인 통보가 403 이다.
INDEXNOW_KEY: ${INDEXNOW_KEY:-} INDEXNOW_KEY: ${INDEXNOW_KEY:-}
@ -48,10 +52,10 @@ services:
# ★ 크론은 이 컨테이너에서만 돈다. 프로세스가 여럿이면 같은 시각에 중복 실행된다. # ★ 크론은 이 컨테이너에서만 돈다. 프로세스가 여럿이면 같은 시각에 중복 실행된다.
SCHEDULER_ENABLED: "1" SCHEDULER_ENABLED: "1"
volumes: volumes:
- ./solution/site/payloads:/app/out/payloads - ./solution/site/payloads:/app/solution/site/payloads
# 노래 파일. payload 와 나란히 둔다 — 백엔드가 mp3 를 여기 떨구면 프리렌더가 # 노래 파일. payload 와 나란히 둔다 — 백엔드가 mp3 를 여기 떨구면 워커가 렌더러를
# 사이트 디렉토리로 복사한다(services/song_service · site/scripts/prerender.ts). # 돌릴 때 사이트 디렉토리로 복사된다(services/song_service · site/scripts/prerender.ts).
- ./solution/site/songs:/app/out/songs - ./solution/site/songs:/app/solution/site/songs
# ★ 스키마 마이그레이션 SQL. 이미지에 굽지 않고 마운트한다 — 파일이 자주 늘고, # ★ 스키마 마이그레이션 SQL. 이미지에 굽지 않고 마운트한다 — 파일이 자주 늘고,
# 이미 세운 DB 를 따라오게 하는 것이 목적이라 코드 배포와 별개로 돌 수 있어야 한다. # 이미 세운 DB 를 따라오게 하는 것이 목적이라 코드 배포와 별개로 돌 수 있어야 한다.
- ./postgres-init:/app/postgres-init:ro - ./postgres-init:/app/postgres-init:ro
@ -64,13 +68,17 @@ services:
driver: json-file driver: json-file
options: { max-size: "10m", max-file: "5" } options: { max-size: "10m", max-file: "5" }
# ★ BUILD·ROLLBACK 잡이 렌더러(solution/site)를 subprocess 로 직접 돌린다
# (services/render_service.py) — 예전에 solution-prerender 컨테이너가 하던 일이다.
# 그래서 이 서비스만 **다른 이미지**(Dockerfile.worker, Node 런타임 + 컴파일된 렌더러
# 포함)를 쓴다. solution-backend·admin-backend 는 그 Node 를 쓸 일이 없다.
solution-worker: solution-worker:
build: build:
context: . context: .
dockerfile: solution/backend/Dockerfile dockerfile: solution/backend/Dockerfile.worker
image: o2o-web4ai-backend image: o2o-web4ai-worker
container_name: o2o-web4ai-solution-worker container_name: o2o-web4ai-solution-worker
command: ["python", "worker_main.py"] command: ["sh", "-c", "node /app/solution/site/dist/prerender/prerender.js --seed-assets --out=/app/solution/site/out && exec python worker_main.py"]
env_file: env_file:
- .env - .env
environment: environment:
@ -79,15 +87,20 @@ services:
WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-1} WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-1}
JOB_DEADLINE_SEC: ${JOB_DEADLINE_SEC:-900} JOB_DEADLINE_SEC: ${JOB_DEADLINE_SEC:-900}
JOB_LEASE_SEC: ${JOB_LEASE_SEC:-120} JOB_LEASE_SEC: ${JOB_LEASE_SEC:-120}
# 렌더러 subprocess 가 굽는 동안 기다리는 시간(사진 내려받기 포함). BUILD 의
# job_deadline_sec 보다 짧아야 한다 — 안 그러면 잡 전체가 먼저 타임아웃된다.
RENDER_TIMEOUT_SEC: ${RENDER_TIMEOUT_SEC:-180}
# 이미지의 HEALTHCHECK 는 API 용(HTTP :9800)이다. 워커는 포트가 없어 그대로 두면 늘 unhealthy 다. # 이미지의 HEALTHCHECK 는 API 용(HTTP :9800)이다. 워커는 포트가 없어 그대로 두면 늘 unhealthy 다.
healthcheck: healthcheck:
disable: true disable: true
volumes: volumes:
- ./solution/site/payloads:/app/out/payloads - ./solution/site/payloads:/app/solution/site/payloads
# 노래 파일. payload 와 나란히 둔다 — 백엔드가 mp3 를 여기 떨구면 프리렌더가 # 노래 파일. payload 와 나란히 둔다 — 백엔드가 mp3 를 여기 떨구면 렌더러가
# 사이트 디렉토리로 복사한다(services/song_service · site/scripts/prerender.ts). # 사이트 디렉토리로 복사한다(services/song_service · site/scripts/prerender.ts).
- ./solution/site/songs:/app/out/songs - ./solution/site/songs:/app/solution/site/songs
- site-out:/app/out/sites:ro # ★ 이제 이 컨테이너가 굽는 쪽이다 — 읽기전용이 아니다(예전 solution-prerender 가
# 쓰던 자리를 그대로 이어받는다).
- site-out:/app/solution/site/out
extra_hosts: extra_hosts:
- "host.docker.internal:host-gateway" - "host.docker.internal:host-gateway"
stop_grace_period: 300s stop_grace_period: 300s
@ -124,10 +137,10 @@ services:
start_period: 20s start_period: 20s
retries: 3 retries: 3
volumes: volumes:
- ./solution/site/payloads:/app/out/payloads - ./solution/site/payloads:/app/solution/site/payloads
# 노래 파일. payload 와 나란히 둔다 — 백엔드가 mp3 를 여기 떨구면 프리렌더가 # 노래 파일. payload 와 나란히 둔다 — 백엔드가 mp3 를 여기 떨구면 프리렌더가
# 사이트 디렉토리로 복사한다(services/song_service · site/scripts/prerender.ts). # 사이트 디렉토리로 복사한다(services/song_service · site/scripts/prerender.ts).
- ./solution/site/songs:/app/out/songs - ./solution/site/songs:/app/solution/site/songs
ports: ports:
# ★ 내부망에만 연다. 0.0.0.0 으로 열면 API 를 가른 의미가 없다. # ★ 내부망에만 연다. 0.0.0.0 으로 열면 API 를 가른 의미가 없다.
- "${ADMIN_API_BIND:-127.0.0.1}:${ADMIN_API_PORT_PUBLIC:-9801}:9801" - "${ADMIN_API_BIND:-127.0.0.1}:${ADMIN_API_PORT_PUBLIC:-9801}:9801"
@ -140,48 +153,6 @@ services:
driver: json-file driver: json-file
options: { max-size: "10m", max-file: "5" } options: { max-size: "10m", max-file: "5" }
# 발행 사이트를 굽는다. **굽기만 한다** — 서빙은 solution-site(nginx)가 맡는다.
#
# ★ 사장님 앱 dev 서버는 여기서 빠졌다. 운영에 `vite dev` 를 띄우면 요청마다 트랜스파일하고
# 기동이 npm install 네트워크에 의존하고 /src 원본이 그대로 나간다. 번들은 solution-site
# 이미지가 굽는다(nginx/Dockerfile). 로컬에서 HMR 이 필요하면 `--profile dev`.
solution-prerender:
image: node:24-alpine
container_name: o2o-web4ai-solution-prerender
working_dir: /app
command:
- sh
- -c
- |
cd /app
# ★ `-d node_modules` 로 판단하면 안 된다. 익명 볼륨은 빈 디렉토리로 이미 존재해서
# 설치를 건너뛰고 `vite: not found`(exit 127)로 죽는다.
[ -x node_modules/.bin/vite ] || npm install
exec node solution/site/scripts/watch-payloads.mjs
environment:
INDEXNOW_KEY: ${INDEXNOW_KEY:-}
# ★ 발행 호스트를 프론트 .env 에 따로 적지 않는다 — 루트 .env 의 SITE_PUBLIC_HOST 를
# 그대로 흘려보낸다. 두 곳에 적으면 canonical 과 화면 주소가 조용히 갈라진다.
VITE_PUBLISH_HOST: ${SITE_PUBLIC_HOST:-localhost}
volumes:
- ./package.json:/app/package.json
- ./package-lock.json:/app/package-lock.json
- ./tsconfig.base.json:/app/tsconfig.base.json
- ./solution:/app/solution
- ./admin:/app/admin
# ★ node_modules 는 컨테이너 것을 쓴다. 호스트가 macOS(arm64-darwin)라 그 안의
# rollup·esbuild 네이티브 바이너리를 리눅스 컨테이너가 못 쓴다.
- /app/node_modules
- /app/solution/site/node_modules
- /app/solution/frontend/node_modules
- /app/admin/frontend/node_modules
# ★ 산출물은 named volume. 호스트 경로면 재배포로 코드를 갈아엎는 순간 전 사이트가 404 다.
- site-out:/app/solution/site/out
restart: unless-stopped
logging:
driver: json-file
options: { max-size: "10m", max-file: "5" }
# 사장님 앱 dev 서버 + 발행본 정적서버(:3001). **로컬 전용**이다 — `--profile dev`. # 사장님 앱 dev 서버 + 발행본 정적서버(:3001). **로컬 전용**이다 — `--profile dev`.
# 운영에서 이게 뜨면 안 된다(위 solution-prerender 주석). # 운영에서 이게 뜨면 안 된다(위 solution-prerender 주석).
solution-frontend: solution-frontend:
@ -294,7 +265,7 @@ services:
# 앞단 프록시를 세울 거면 여기서 포트만 옮기고 프록시가 이쪽을 가리키게 한다. # 앞단 프록시를 세울 거면 여기서 포트만 옮기고 프록시가 이쪽을 가리키게 한다.
- "${SITE_HTTP_BIND:-0.0.0.0}:${SITE_HTTP_PORT:-80}:80" - "${SITE_HTTP_BIND:-0.0.0.0}:${SITE_HTTP_PORT:-80}:80"
depends_on: depends_on:
- solution-prerender - solution-backend
restart: unless-stopped restart: unless-stopped
logging: logging:
driver: json-file driver: json-file

View File

@ -1,5 +1,8 @@
# ARCHITECTURE # ARCHITECTURE
> 2026-09-15: 현재 발행 실행·산출물 버전·배포는 [PUBLISH_VERSION.md](PUBLISH_VERSION.md).
> 아래의 별도 프리렌더 컨테이너·폴링·전체 재굽기 설명은 이전 구조다.
제품 판단은 [PRODUCT.md](PRODUCT.md), 배포 절차는 [DEPLOY.md](DEPLOY.md), 제품 판단은 [PRODUCT.md](PRODUCT.md), 배포 절차는 [DEPLOY.md](DEPLOY.md),
에이전트가 밟기 쉬운 함정 목록은 [AGENTS.md](../AGENTS.md). 여기는 **구조와 경계**만 다룬다. 에이전트가 밟기 쉬운 함정 목록은 [AGENTS.md](../AGENTS.md). 여기는 **구조와 경계**만 다룬다.

View File

@ -1,5 +1,8 @@
# 배포 · 스토리지 # 배포 · 스토리지
> 2026-09-15 이후 절차는 [PUBLISH_VERSION.md](PUBLISH_VERSION.md)를 따른다.
> 기존 사이트 전체 재굽기는 하지 않는다. 최초 전환 때 구 프리렌더를 중지한다.
> **현재 결정 (2026-08-31): 발행 사이트는 서버 안에서 nginx 가 정적 서빙한다.** > **현재 결정 (2026-08-31): 발행 사이트는 서버 안에서 nginx 가 정적 서빙한다.**
> Azure Blob 은 코드에 있으나 **켜지 않는다**(`AZURE_STORAGE_CONNECTION_STRING` 비움). > Azure Blob 은 코드에 있으나 **켜지 않는다**(`AZURE_STORAGE_CONNECTION_STRING` 비움).
> 클라우드는 고도화 때 붙인다 — 근거는 [ARCHITECTURE.md 3절](ARCHITECTURE.md). > 클라우드는 고도화 때 붙인다 — 근거는 [ARCHITECTURE.md 3절](ARCHITECTURE.md).

View File

@ -1,5 +1,14 @@
# 개발 일지 # 개발 일지
## 2026-09-15 — 워커 렌더·발행 버전·예약 안내·미리보기 대기
- 상시 프리렌더를 제거하고 워커가 컴파일된 Node 렌더러를 실행한다.
- 버전별 HTML을 보존하고 게이트 통과 뒤 공개 링크를 전환한다. 재시도는 저장된 성공본을 사용한다.
- 예약 전 확인을 이용안내에 통합하고 iframe 렌더 완료까지 스피너를 표시한다.
- 배포는 기존 HTML과 목업을 재굽지 않는다. 상세: [PUBLISH_VERSION.md](PUBLISH_VERSION.md).
- 읽기 생성 토큰 상한을 늘리고 추첨 배열을 고정해 반복 렌더를 방지한다.
- 검증: 사이트 81건, 백엔드 발행·롤백·서치콘솔 45건 통과. 빌더·사이트 빌드 통과.
무엇을 왜 바꿨는지 날짜순으로 남긴다. 새 항목을 **위에** 추가한다. 무엇을 왜 바꿨는지 날짜순으로 남긴다. 새 항목을 **위에** 추가한다.
결론과 배경은 각 문서가 단일 출처다 — 여기에는 요약과 링크만 둔다. 결론과 배경은 각 문서가 단일 출처다 — 여기에는 요약과 링크만 둔다.

32
docs/PUBLISH_VERSION.md Normal file
View File

@ -0,0 +1,32 @@
# 발행 버전과 워커 (2026-09-15)
이 문서가 이전 문서의 프리렌더 상시 기동·전체 재굽기 절차를 대체한다.
`BUILD → snapshot → payload → Node 렌더 → 결과 게이트 → 공개 링크 전환 → DB 기록`
- Python은 HTML을 만들지 않는다. 미리 컴파일된 Node를 실행하고 JSON 보고서만 읽는다.
- 워커 이미지에 Node와 렌더러를 포함한다. 실행 중 npm 설치·번들 빌드는 없다.
- 공유 볼륨의 파일 잠금으로 렌더·공개 전환을 직렬화한다. 수집 등 다른 잡은 동시 실행한다.
- `out/versions/<slug>/<version>`에 성공한 HTML과 렌더 보고서를 보존한다.
- `out/s/<slug>`는 공개 버전의 상대 심볼릭 링크다. 게이트 통과 후 전환한다.
- 성공한 버전은 재시도·롤백 때 다시 쓰지 않는다. 기존 일반 디렉토리는 첫 재발행 때 legacy로 보존한다.
- 배포 시 HTML·기존 HTML의 자산 주소를 수정하지 않는다. 공용 자산과 미리보기 셸만 준비한다.
- 목업과 보관 버전의 참조 자산도 삭제 대상에서 제외한다. versions는 Azure 공용 업로드에서 제외한다.
- 롤백 API: `POST /v1/place/{place_id}/site/version/rollback`, `target_version`. 소유권 검사와 BUILD 중복 방지 키를 공유한다.
- 로컬 공개 전환과 DB/Azure는 단일 트랜잭션이 아니다. 외부 저장소·DB 실패 시 재시도 및 운영 확인이 필요하다.
- 최초 일반 디렉토리→링크 전환은 두 rename 사이 짧은 공백이 가능하다. 이후 링크 교체는 원자적이다.
## 배포
1. 진행 중 잡·서버 변경·목업 및 참조 자산 해시를 확인하고 site-out을 백업한다.
2. backend·worker·site 이미지를 빌드한다. admin은 기본 대상이 아니다.
3. 기존 worker와 solution-prerender를 중지한 뒤 새 worker를 기동한다. 두 렌더러를 동시에 실행하지 않는다.
4. API·미리보기·테스트 발행을 확인하고 목업 해시를 대조한다. 전체 재굽기·republish_all은 실행하지 않는다.
워커 경로: `SITE_PAYLOAD_DIR=/app/solution/site/payloads`, `SITE_OUTPUT_DIR=/app/solution/site/out`.
DB 테이블 추가는 없다. 기존 버전·잡·발행 로그를 사용한다.
## UI
예약 전 확인과 요약을 이용안내 및 예약에 통합한다. 별도 요약 섹션과 예약 카드의 중복 규정은 제거한다.
빌더 미리보기는 iframe 내부 렌더 완료 신호까지 스피너를 표시한다. 출처·iframe을 확인하고 12초 상한을 둔다.

View File

@ -93,7 +93,7 @@ CREATE TABLE IF NOT EXISTS public.users (
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS public.jobs ( CREATE TABLE IF NOT EXISTS public.jobs (
job_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- ★ 이 표만 server_default 가 꼭 필요하다 — 큐 전이가 raw SQL(RETURNING)이라 ORM 의 파이썬 default 가 안 먹는다 job_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- ★ 이 표만 server_default 가 꼭 필요하다 — 큐 전이가 raw SQL(RETURNING)이라 ORM 의 파이썬 default 가 안 먹는다
job_type SMALLINT NOT NULL, -- JobType: 1=collect 2=vision 3=copy 4=build 5=local_sync 6=ai_check job_type SMALLINT NOT NULL, -- JobType: 1=collect 2=vision 3=copy 4=build 5=local_sync 6=ai_check 7=song 8=rollback
status SMALLINT NOT NULL DEFAULT 1, -- JobStatus: 1=pending 2=running 3=done 4=dead status SMALLINT NOT NULL DEFAULT 1, -- JobStatus: 1=pending 2=running 3=done 4=dead
priority SMALLINT NOT NULL DEFAULT 100, -- 낮을수록 우선 priority SMALLINT NOT NULL DEFAULT 100, -- 낮을수록 우선
payload JSONB NOT NULL DEFAULT '{}'::jsonb, payload JSONB NOT NULL DEFAULT '{}'::jsonb,

View File

@ -0,0 +1,70 @@
# 워커 전용 이미지 — solution-backend(:9800)·admin-backend(:9801) 와 **다른 이미지**다.
#
# ★ 왜 갈랐나 (프리렌더를 워커 실행으로 통합, 2026-09-15)
# BUILD·ROLLBACK 잡은 이제 컴파일된 Node 렌더러(solution/site)를 subprocess 로 직접
# 돌린다(services/render_service.py) — 예전에 별도 컨테이너(solution-prerender)가 하던
# 일이다. 그 렌더러가 필요한 건 **워커뿐**이다. Dockerfile(api/admin 겸용)에 Node 를
# 같이 넣으면 API 를 여는 요청만 받는 컨테이너에도 쓰지 않는 Node 런타임과 렌더러
# 번들이 실려 나간다 — 이미지가 커지고 공격 표면도 는다.
#
# ★ 기동·잡 실행 중에 npm install 을 하지 않는다. site-builder 스테이지가 이미지 빌드
# 시점에 한 번 굽고, 최종 이미지는 컴파일된 산출물만 COPY 한다.
# ★ node_modules 는 안 들어간다 — solution/site/vite.config.ts 가 SSR 번들에
# `ssr.noExternal: true` 로 react 등 의존성까지 전부 접어 넣는다(실측: node_modules 를
# 지우고 실행해도 그대로 돈다, dist/prerender/prerender.js 단일 파일 1.8MB).
#
# ★ 빌드 컨텍스트는 solution-backend 와 같은 **레포 루트**다 — solution/site 와
# solution/backend 를 한 이미지에 담아야 한다.
# ── 1단계: 렌더러 컴파일 (Node) ──────────────────────────────────────────────
FROM node:24-bookworm AS site-builder
WORKDIR /app
COPY package.json package-lock.json tsconfig.base.json ./
COPY solution/shared/package.json solution/shared/
COPY solution/frontend/package.json solution/frontend/
COPY solution/site/package.json solution/site/
COPY admin/frontend/package.json admin/frontend/
# ★ npm workspaces 는 루트 package.json 의 workspaces 목록에 있는 자리마다 package.json 을
# 요구한다 — site 만 빌드해도 4개를 전부 COPY 해야 npm ci 가 성공한다(nginx/Dockerfile 과 같다).
RUN npm ci
COPY solution/shared ./solution/shared
COPY solution/site ./solution/site
RUN npm run build:client -w @o2o/site \
&& npm run build:prerender -w @o2o/site
# ── 2단계: Node 런타임만 (바이너리를 뽑아 쓴다) ──────────────────────────────
# ★ python:3.12-slim 과 같은 계열(Debian bookworm)의 태그를 써야 한다 — glibc ABI 가
# 어긋나면 바이너리만 복사해서는 못 돈다("cannot execute binary file" 류로 죽는다).
FROM node:24-bookworm-slim AS node-runtime
# ── 3단계: 최종 워커 이미지 ───────────────────────────────────────────────
FROM python:3.12-slim-bookworm
WORKDIR /app
COPY solution/backend/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# ★ yanolja_adapter.py 가 Playwright 로 페이지를 렌더링한다(수집 파이프라인, 워커 전용 경로) —
# 패키지(pip)만으로는 브라우저 실행 파일이 없다. --with-deps 가 Chromium 이 필요로 하는
# 시스템 라이브러리(libnss3 등)까지 apt 로 같이 깐다.
RUN playwright install --with-deps chromium
# Node 바이너리만 옮겨 온다 — npm·corepack 은 쓰지 않는다(빌드는 site-builder 스테이지가
# 이미 끝냈다). /usr/local/bin/node 하나로 `node dist/prerender/prerender.js` 를 돌리는
# 데 충분하다(실측: 아래 COPY 만으로 `node --version` 과 렌더 스모크 테스트가 통과한다).
COPY --from=node-runtime /usr/local/bin/node /usr/local/bin/node
COPY solution/backend ./solution/backend
# ★ SITE_ROOT(solution/site/scripts/prerender.ts)가 자기 파일 위치 기준 상대경로로
# payloads·songs·out 을 찾는다 — dist·public 이 이 자리(/app/solution/site/)에 있어야
# 워커가 컨테이너 안에서 렌더러를 그대로 실행할 수 있다.
COPY --from=site-builder /app/solution/site/dist ./solution/site/dist
COPY --from=site-builder /app/solution/site/public ./solution/site/public
ENV APP_ENV=local
ENV PYTHONPATH=/app/solution/backend
WORKDIR /app/solution/backend
# 워커는 포트가 없다 — 이미지 공통 HEALTHCHECK 를 두지 않는다(compose 가 disable 한다).
CMD ["python", "worker_main.py"]

View File

@ -389,6 +389,7 @@ class PublishAction(CodeEnum):
REBUILD = 3 REBUILD = 3
SUSPEND = 4 SUSPEND = 4
RESUME = 5 RESUME = 5
ROLLBACK = 6 # 예전 버전으로 공개 주소를 되돌림 — services/rollback_service.py
class PublishResult(CodeEnum): class PublishResult(CodeEnum):
@ -434,6 +435,7 @@ class JobType(CodeEnum):
LOCAL_SYNC = 5 # 지역 정보 갱신 — 행정구역 코드 단위(같은 지역 사이트 50개여도 1회) LOCAL_SYNC = 5 # 지역 정보 갱신 — 행정구역 코드 단위(같은 지역 사이트 50개여도 1회)
AI_CHECK = 6 # AI 검색 노출 점검 AI_CHECK = 6 # AI 검색 노출 점검
SONG = 7 # 이 숙소의 노래 한 곡 (가사 Gemini → 작곡 Suno). 발행이 이 잡을 건다 SONG = 7 # 이 숙소의 노래 한 곡 (가사 Gemini → 작곡 Suno). 발행이 이 잡을 건다
ROLLBACK = 8 # 예전 버전 스냅샷으로 다시 굽고 공개 주소를 그 버전으로 되돌림
class JobStatus(CodeEnum): class JobStatus(CodeEnum):

View File

@ -166,7 +166,7 @@ def fake_renderer(monkeypatch, tmp_path_factory):
★ 왜 필요한가 ★ 왜 필요한가
발행 게이트는 이제 **실제로 나갈 HTML** 을 보고 판정한다. 그 HTML 은 Node 렌더러가 발행 게이트는 이제 **실제로 나갈 HTML** 을 보고 판정한다. 그 HTML 은 Node 렌더러가
굽고, BUILD 잡은 그 결과 보고서를 기다린다(services/render_report.wait_for). 굽고, BUILD 잡은 렌더러 subprocess 를 직접 돌린다(services/render_service.render_site).
파이썬 테스트 환경에는 Node 렌더러가 없으므로, payload 를 읽어 보고서를 만들어 주는 파이썬 테스트 환경에는 Node 렌더러가 없으므로, payload 를 읽어 보고서를 만들어 주는
대역을 끼운다 — 여기서 검사하려는 건 **백엔드가 보고서를 어떻게 처리하는가** 다. 대역을 끼운다 — 여기서 검사하려는 건 **백엔드가 보고서를 어떻게 처리하는가** 다.
@ -175,8 +175,9 @@ def fake_renderer(monkeypatch, tmp_path_factory):
두 벌로 두면 어긋나고, 어긋난 걸 아무도 모르는 게 원래 문제였다. 두 벌로 두면 어긋나고, 어긋난 걸 아무도 모르는 게 원래 문제였다.
""" """
import json import json
from pathlib import Path
from services import render_report, site_payload from services import render_report, render_service, site_payload
payload_dir = tmp_path_factory.mktemp("payloads") payload_dir = tmp_path_factory.mktemp("payloads")
monkeypatch.setattr(site_payload, "payload_dir", lambda: payload_dir) monkeypatch.setattr(site_payload, "payload_dir", lambda: payload_dir)
@ -229,13 +230,16 @@ def fake_renderer(monkeypatch, tmp_path_factory):
count += sum(1 for m in payload.get("media") or [] if long(m.get("alt"))) count += sum(1 for m in payload.get("media") or [] if long(m.get("alt")))
return count return count
async def _wait_for(slug, site_version, timeout_sec, poll_sec=1.0): async def _render_site(payload_path, site_version, timeout_sec):
path = payload_dir / f"{slug}.json" path = Path(payload_path)
slug = path.stem
if not path.is_file(): if not path.is_file():
return None raise render_service.RenderFailed(f"payload 파일이 없다: {payload_path}")
payload = json.loads(path.read_text(encoding="utf-8")) payload = json.loads(path.read_text(encoding="utf-8"))
if payload["site"]["version"] != site_version: if payload["site"]["version"] != site_version:
return None raise render_service.RenderFailed(
f"버전이 다르다(payload={payload['site']['version']} 요청={site_version})"
)
place = payload.get("place") or {} place = payload.get("place") or {}
count = _count(payload) count = _count(payload)
# ★ 고유 콘텐츠 0건이면 렌더러는 **페이지를 쓰지 않는다** # ★ 고유 콘텐츠 0건이면 렌더러는 **페이지를 쓰지 않는다**
@ -276,5 +280,8 @@ def fake_renderer(monkeypatch, tmp_path_factory):
"error": None, "error": None,
} }
monkeypatch.setattr(render_report, "wait_for", _wait_for) monkeypatch.setattr(render_service, "render_site", _render_site)
async def _activate_site(slug, site_version):
return None
monkeypatch.setattr(render_service, "activate_site", _activate_site)
return payload_dir return payload_dir

View File

@ -45,6 +45,10 @@ class ISiteCRUD(ABC):
async def get_version(self, cdb: AsyncSession, site_id, site_version_id) -> Tuple[ErrorType, site_versions]: async def get_version(self, cdb: AsyncSession, site_id, site_version_id) -> Tuple[ErrorType, site_versions]:
pass pass
@abstractmethod
async def get_version_by_number(self, cdb: AsyncSession, site_id, version: int) -> Tuple[ErrorType, site_versions]:
pass
@abstractmethod @abstractmethod
async def finish_version(self, cdb: AsyncSession, site_version_id, data: dict) -> Tuple[ErrorType, int]: async def finish_version(self, cdb: AsyncSession, site_version_id, data: dict) -> Tuple[ErrorType, int]:
pass pass
@ -189,6 +193,31 @@ class SiteCRUD(ISiteCRUD):
LOG.e_no_callstack(ex) LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, None return ErrorType.DB_RUN_FAILED, None
async def get_version_by_number(self, cdb: AsyncSession, site_id, version: int) -> Tuple[ErrorType, site_versions]:
"""롤백 대상 조회 — site_versions.version(사람이 보는 번호) 로 찾는다.
★ site_version_id(uuid) 가 아니다. 화면·API 는 버전 번호로 고르는 게 자연스럽고,
그 번호가 곧 out/versions/<slug>/<version>/ 디렉토리 이름이다(prerender.ts)."""
try:
query = (
select(site_versions)
.where(
site_versions.site_id == site_id,
site_versions.version == version,
site_versions.deleted == False, # noqa: E712
)
.limit(1)
)
err_type, rows = await DB_SESSION_MNG.execute(cdb, query)
if err_type != ErrorType.SUCCESS:
return err_type, None
if len(rows) != 1:
return ErrorType.DB_INVALID_KEY, None
return ErrorType.SUCCESS, rows[0]
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, None
async def finish_version(self, cdb: AsyncSession, site_version_id, data: dict) -> Tuple[ErrorType, int]: async def finish_version(self, cdb: AsyncSession, site_version_id, data: dict) -> Tuple[ErrorType, int]:
"""빌드 결과를 버전에 기록한다(BUILT 또는 FAILED).""" """빌드 결과를 버전에 기록한다(BUILT 또는 FAILED)."""
try: try:

View File

@ -36,6 +36,17 @@ class Res_StartBuild(Res_WebPacketProtocol):
created: bool = True created: bool = True
class Req_Rollback(SiteProtocol):
"""예전 버전으로 공개 주소를 되돌린다.
★ 재굽기가 아니다 — 대상 버전이 디스크에 아직 있으면 심볼릭 링크만 돌린다. 지워졌으면
(보관 정책, prerender.ts pruneOldVersions) site_versions.snapshot 으로 다시 굽고 나서
돌린다. 어느 경우든 게이트를 다시 통과해야 한다(사장님이 이미 확인한 값이라 대개는
그대로 통과한다)."""
target_version: int
class SiteVersionData(WebPacketProtocol): class SiteVersionData(WebPacketProtocol):
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)

View File

@ -7,6 +7,7 @@ from common.models.gmodel import PageParams, UserInfo
from router.v1.validator.dependencies import IsValidAccessToken, RemoveNoneResponse from router.v1.validator.dependencies import IsValidAccessToken, RemoveNoneResponse
from services.site_service import SiteService from services.site_service import SiteService
from .protocol import ( from .protocol import (
Req_Rollback,
Req_SiteSlug, Req_SiteSlug,
Req_SiteStatus, Req_SiteStatus,
Req_SiteTemplate, Req_SiteTemplate,
@ -178,6 +179,20 @@ async def site_preview(
return JSONResponse(content=payload) return JSONResponse(content=payload)
@router.post(
path="/version/rollback",
response_model=Res_StartBuild,
summary="예전 버전으로 되돌리기(비동기)",
description="target_version 을 다시 공개 주소로 내보낸다. 재수집·재생성을 하지 않는다 — "
"그 버전이 발행될 때 박제된 snapshot 을 그대로 다시 굽는다. 렌더러가 그 사이 "
"바뀌었을 수 있어 게이트는 다시 통과해야 한다. 실패하면 지금 공개된 버전은 그대로다.",
)
async def rollback_version(
place_id: UUID, req: Req_Rollback, service: SiteService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)
):
return RemoveNoneResponse(await service.start_rollback(user_info, str(place_id), req))
@router.get(path="/version/list", response_model=Res_SiteVersions, summary="빌드 버전 목록") @router.get(path="/version/list", response_model=Res_SiteVersions, summary="빌드 버전 목록")
async def list_versions(place_id: UUID, service: SiteService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)): async def list_versions(place_id: UUID, service: SiteService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
return RemoveNoneResponse(await service.list_versions(user_info, str(place_id))) return RemoveNoneResponse(await service.list_versions(user_info, str(place_id)))

View File

@ -22,7 +22,7 @@ def is_configured() -> bool:
def output_dir() -> Path: def output_dir() -> Path:
return Path(os.environ.get("SITE_OUTPUT_DIR", "/app/out/sites")) return Path(os.environ.get("SITE_OUTPUT_DIR", "/app/solution/site/out"))
def _content_type(path: Path) -> str: def _content_type(path: Path) -> str:
@ -104,7 +104,7 @@ def _upload_shared(container, root: Path, prefix: str) -> set[str]:
for entry in sorted(root.iterdir()): for entry in sorted(root.iterdir()):
# `s/` 는 사이트별 디렉터리다 — 발행한 사이트 하나만 따로 올린다. # `s/` 는 사이트별 디렉터리다 — 발행한 사이트 하나만 따로 올린다.
# (여기서 함께 올리면 한 명이 발행할 때마다 전체 사이트를 다시 올리게 된다.) # (여기서 함께 올리면 한 명이 발행할 때마다 전체 사이트를 다시 올리게 된다.)
if entry.name.startswith(".") or entry.name == SITE_ROOT_DIR: if entry.name.startswith(".") or entry.name in {SITE_ROOT_DIR, "versions"}:
continue continue
if entry.is_dir(): if entry.is_dir():
uploaded |= _upload_tree(container, root, entry.name, prefix) uploaded |= _upload_tree(container, root, entry.name, prefix)

View File

@ -33,7 +33,7 @@ from services import (
azure_static, azure_static,
indexnow, indexnow,
publish_gate, publish_gate,
render_report, render_service,
seo_keywords, seo_keywords,
site_payload, site_payload,
site_thumbnail, site_thumbnail,
@ -46,9 +46,9 @@ from services.snapshot import build_snapshot
_site_crud = SiteCRUD() _site_crud = SiteCRUD()
_place_crud = PlaceCRUD() _place_crud = PlaceCRUD()
# 렌더러가 이 버전을 굽고 보고서를 돌려줄 때까지 기다리는 시간. # 렌더러 subprocess 가 끝나기를 기다리는 시간(사진 내려받기 포함).
# ★ 넉넉해야 한다. 짧으면 멀쩡한 발행이 "렌더 대기 초과"로 실패한다 — # ★ 넉넉해야 한다. 짧으면 멀쩡한 발행이 "렌더 시간 초과"로 실패한다 — 처음 보는 사진을
# 렌더러는 payload 를 2초 주기로 보고, 사이트 하나 굽는 데 1초 남짓 걸린다. # 내려받는 발행은 몇 초가 더 걸린다(mirrorMedia, prerender.ts).
RENDER_TIMEOUT_SEC = float(os.environ.get("RENDER_TIMEOUT_SEC") or 180) RENDER_TIMEOUT_SEC = float(os.environ.get("RENDER_TIMEOUT_SEC") or 180)
@ -73,10 +73,13 @@ async def ensure_site(place_id: str) -> "sites":
return row return row
async def _load_links(place_id: str) -> list: async def load_channel_links(place_id: str) -> list:
"""채널 링크(야놀자·네이버 플레이스·인스타…). """채널 링크(야놀자·네이버 플레이스·인스타…).
스냅샷에 담기지 않는 유일한 발행 재료라 여기서 읽어 payload 로 넘긴다. 스냅샷에 담기지 않는 유일한 발행 재료라 여기서 읽어 payload 로 넘긴다 — 재빌드(run_build)
도 롤백(rollback_service.run_rollback)도 같은 함수를 쓴다. **스냅샷에는 안 싣는다** —
롤백이 옛 스냅샷으로 다시 구워도 링크는 항상 지금 확정된 것을 보여줘야 한다(끊긴 링크를
옛 버전째 되살리면 안 된다).
★ 실패해도 빈 목록으로 진행한다 — 링크가 없다고 발행을 막을 이유가 없다.""" ★ 실패해도 빈 목록으로 진행한다 — 링크가 없다고 발행을 막을 이유가 없다."""
err, rows = await DB_SESSION_MNG.execute_lambda( err, rows = await DB_SESSION_MNG.execute_lambda(
place_channels.DBType(), place_channels.DBType(),
@ -249,24 +252,25 @@ async def run_build(job: dict) -> dict:
site.status = SiteStatus.PUBLISHED.value site.status = SiteStatus.PUBLISHED.value
site.published_at = site.published_at or now site.published_at = site.published_at or now
links = await _load_links(place_id) links = await load_channel_links(place_id)
payload_path = await emit_payload(place, snapshot, site, version, links) payload_path = await emit_payload(place, snapshot, site, version, links, publish=want_publish)
if not payload_path: if not payload_path:
return await _fail("payload 를 쓰지 못했다 — 렌더러에 넘길 입력이 없다") return await _fail("payload 를 쓰지 못했다 — 렌더러에 넘길 입력이 없다")
result["payload_path"] = payload_path result["payload_path"] = payload_path
slug = site_payload.publish_slug(place, site) slug = site_payload.publish_slug(place, site)
# ---- 렌더 결과를 기다린다 ---- # ---- 렌더러를 직접 돌린다 ----
# ★ 게이트는 **실제로 나갈 HTML** 을 보고 판정해야 한다. 렌더러가 자기 산출물을 대조해 # ★ 게이트는 **실제로 나갈 HTML** 을 보고 판정해야 한다. 렌더러가 자기 산출물을 대조해
# 구조화 데이터 불일치와 고유 콘텐츠 수를 보고서로 돌려준다. # 구조화 데이터 불일치와 고유 콘텐츠 수를 보고서로 돌려준다.
report = await render_report.wait_for(slug, version_no, RENDER_TIMEOUT_SEC) # ★ payload=False(미발행)면 렌더러는 `out/versions/<slug>/<version>/` 에만 굽고 공개
if report is None: # 주소(`out/s/<slug>`)는 건드리지 않는다 — emit_payload 에 실은 publish 플래그가 정한다.
try:
report = await render_service.render_site(payload_path, version_no, RENDER_TIMEOUT_SEC)
except render_service.RenderFailed as ex:
# ★ 발행하지 않는다. 페이지가 있는지 확인하지 못한 채 "발행됨"으로 남기면 # ★ 발행하지 않는다. 페이지가 있는지 확인하지 못한 채 "발행됨"으로 남기면
# 사장님은 [사이트 열기] 를 눌러야 404 로 알게 된다. # 사장님은 [사이트 열기] 를 눌러야 404 로 알게 된다.
return await _fail( return await _fail(str(ex))
f"렌더 결과를 {RENDER_TIMEOUT_SEC}초 안에 받지 못했다 — 렌더러가 도는지 확인이 필요하다"
)
mismatches = list(report.get("mismatches") or []) mismatches = list(report.get("mismatches") or [])
# ★ None(재지 못했다)과 0(재 봤더니 0건)을 뭉개지 않는다. 게이트는 raw 를 보고, # ★ None(재지 못했다)과 0(재 봤더니 0건)을 뭉개지 않는다. 게이트는 raw 를 보고,
@ -308,8 +312,9 @@ async def run_build(job: dict) -> dict:
thumbnail_url = None thumbnail_url = None
if want_publish: if want_publish:
try: try:
await render_service.activate_site(slug, version_no)
azure_result = await azure_static.publish(slug) azure_result = await azure_static.publish(slug)
except azure_static.AzurePublishError as ex: except (azure_static.AzurePublishError, render_service.RenderFailed) as ex:
return await _fail(str(ex), None, stamp) return await _fail(str(ex), None, stamp)
if azure_result: if azure_result:
result["azure"] = azure_result result["azure"] = azure_result
@ -383,4 +388,3 @@ async def run_build(job: dict) -> dict:
version.build_status = BuildStatus.BUILT.value version.build_status = BuildStatus.BUILT.value
version.built_at = now version.built_at = now
return result return result

View File

@ -44,7 +44,7 @@ def is_configured() -> bool:
def output_dir() -> Path: def output_dir() -> Path:
return Path(os.environ.get("SITE_OUTPUT_DIR", "/app/out/sites")) return Path(os.environ.get("SITE_OUTPUT_DIR", "/app/solution/site/out"))
def site_urls(slug: str) -> list[str]: def site_urls(slug: str) -> list[str]:

View File

@ -1,22 +1,18 @@
"""렌더 보고서 — SSG 가 실제로 페이지를 구웠는지 확인하는 창구. """렌더 보고서 — SSG 가 실제로 페이지를 구웠는지 확인하는 창구.
★ 왜 필요한가 ★ 왜 필요한가
발행 잡은 payload JSON 을 쓰는 것까지가 끝이고, 방문자가 볼 HTML 은 프론트의 SSG 가 굽는다. BUILD 잡은 payload JSON 을 쓰고 렌더러를 직접 돌린다(services/render_service). 렌더러는
그 사이가 끊겨 있었다. 프리렌더가 깨져도 DB 에는 "발행됨"으로 남고, 페이지는 없는데 굽는 동안 있었던 일(성공/실패, 구조화 데이터 대조, 고유 콘텐츠 수)을 사이트마다
아무도 그걸 모른다 — 사장님이 [사이트 열기] 를 눌러야 404 로 발견된다. `<payload_dir>/.status/<slug>.json` 에 남긴다 — 백엔드에 마운트된 디렉토리가 payload
디렉토리뿐이라 보고서도 그 안에 둔다.
이제 프리렌더가 사이트마다 결과를 `<payload_dir>/.status/<slug>.json` 에 쓴다. ★ 여기서 하는 일 — 그 보고서를 읽는다.
백엔드에 마운트된 디렉토리가 payload 디렉토리뿐이라 보고서도 그 안에 둔다. render_service 가 렌더러 subprocess 를 기다린 **직후** 한 번 읽어 발행 여부를 정하고,
사이트 조회 API(render_status)는 아무 때나 다시 읽어 "지금 이 버전이 구워져 있나" 를 보여준다.
★ 여기서 하는 두 가지
1. 렌더 상태 — 구워졌는가, 언제, 실패했으면 왜 (사이트 조회 API 가 그대로 내보낸다)
2. 발행 대기 — BUILD 잡이 이 버전의 보고서를 기다렸다가, 그 결과로 발행 여부를 정한다
(wait_for). 게이트는 **실제로 나갈 HTML** 을 보고 판정해야 한다.
★ 읽기 전용이다. 보고서가 없거나 깨져 있어도 예외를 밖으로 내보내지 않는다 — ★ 읽기 전용이다. 보고서가 없거나 깨져 있어도 예외를 밖으로 내보내지 않는다 —
사이트 조회가 부수 산출물 때문에 실패하면 안 된다. 사이트 조회가 부수 산출물 때문에 실패하면 안 된다.
""" """
import asyncio
import json import json
from pathlib import Path from pathlib import Path
@ -58,29 +54,6 @@ def read_report(slug: str) -> dict | None:
return report return report
async def wait_for(slug: str, site_version: int, timeout_sec: float, poll_sec: float = 1.0) -> dict | None:
"""이 버전의 렌더 보고서가 나올 때까지 기다린다. 시간 안에 안 나오면 None.
★ 왜 기다리나
발행 게이트는 **실제로 나갈 HTML** 을 보고 판정해야 한다. 그 HTML 은 프론트 렌더러가
굽고, 렌더러는 payload 파일을 보고 도는 별도 프로세스다. 그래서 잡이 payload 를 쓴 뒤
보고서가 올 때까지 기다렸다가, 그 결과로 발행 여부를 정한다.
★ 버전이 맞는 보고서만 받는다. 직전 버전 보고서를 보고 통과시키면 이번에 바뀐 값은
아무도 검사하지 않은 채로 발행된다.
★ BUILD 잡은 heartbeat 로 lease 를 갱신하므로 여기서 기다려도 reaper 에 회수되지 않는다."""
deadline = asyncio.get_running_loop().time() + timeout_sec
while True:
report = read_report(slug)
if report is not None and report.get("siteVersion") == site_version:
return report
if asyncio.get_running_loop().time() >= deadline:
LOG.w(f"[render] 보고서 대기 시간 초과 slug={slug} v{site_version} ({timeout_sec}초)")
return None
await asyncio.sleep(poll_sec)
def render_status(slug: str, site_version: int | None = None) -> dict: def render_status(slug: str, site_version: int | None = None) -> dict:
"""사이트 조회 API 가 그대로 내보내는 렌더 상태. """사이트 조회 API 가 그대로 내보내는 렌더 상태.

View File

@ -0,0 +1,153 @@
"""렌더러 실행 — Python 워커가 미리 컴파일된 Node 렌더러를 subprocess 로 직접 돌린다.
★ 왜 (프리렌더를 워커 실행으로 통합, 2026-09-15)
예전에는 `solution-prerender` 라는 별도 상시 컨테이너가 payloads/ 디렉토리를 2초마다
폴링하며 바뀐 파일을 구웠고, BUILD 잡은 `.status/<slug>.json` 이 나타나기를 또 폴링했다
(render_report.wait_for). 컨테이너 하나·폴링 두 겹이 전부 "굽는 걸 어떻게 아느냐"는
같은 문제를 풀고 있었다 — 워커가 굽기를 직접 돌리면 둘 다 필요 없다: subprocess 가
끝나는 순간이 곧 "구워졌다"는 신호다.
★ 이 파일은 payload → 렌더러 실행 → 구조화된 결과, 그 경계만 다룬다.
Python 은 여전히 HTML 을 만들지 않는다(ARCHITECTURE 1절) — 여기서 하는 일은 미리 빌드된
JS 파일 하나를 `node` 로 실행하고 그 결과 보고서(.status/<slug>.json, prerender.ts 가 쓴다)
를 읽어 돌려주는 것뿐이다. 렌더러 코드 자체는 solution/site 소유 그대로다.
★ 워커 이미지에는 컴파일된 렌더러(`dist/client` · `dist/prerender/prerender.js`)와 `public/`
만 있으면 된다 — node_modules 는 필요 없다(vite.config.ts `ssr.noExternal: true`, 실측:
node_modules 를 지우고 실행해도 그대로 돈다). 기동·잡 실행 중에 npm install 을 하지 않는다.
"""
import asyncio
import fcntl
import os
from pathlib import Path
from common.logger import LOG
from services import render_report
# 컴파일된 렌더러 진입점. Dockerfile 의 site-builder 스테이지가 여기에 굽는다.
RENDERER_ENTRY_ENV = "RENDERER_ENTRY"
DEFAULT_RENDERER_ENTRY = "/app/solution/site/dist/prerender/prerender.js"
NODE_BIN_ENV = "NODE_BIN"
DEFAULT_NODE_BIN = "node"
# 렌더러가 굽는 산출물 루트(out/). services/azure_static·indexnow 와 **같은 env** 를 본다 —
# 렌더러가 쓴 자리와 백엔드가 훑는 자리가 어긋나면 "구웠는데 없다"는 조용한 실패가 난다.
OUTPUT_DIR_ENV = "SITE_OUTPUT_DIR"
DEFAULT_OUTPUT_DIR = "/app/solution/site/out"
class RenderFailed(RuntimeError):
"""렌더러 프로세스 자체가 죽었거나(비정상 종료) 보고서를 남기지 못했다.
★ 게이트 반려(구조화 데이터 불일치·고유 콘텐츠 0건)는 여기 안 걸린다 — 그건 exit code
1 이어도 보고서가 정상적으로 남으므로 호출측이 report.ok 로 판단한다. 이 예외는
"무슨 일이 있었는지조차 모른다" 는 경우만 위한 것이다."""
def renderer_entry() -> str:
return os.environ.get(RENDERER_ENTRY_ENV) or DEFAULT_RENDERER_ENTRY
def output_dir() -> str:
return os.environ.get(OUTPUT_DIR_ENV) or DEFAULT_OUTPUT_DIR
async def _run_subprocess(args: list[str], timeout_sec: float) -> tuple[int | None, str]:
"""같은 볼륨의 자산 대장·사이트맵은 한 프로세스만 갱신한다."""
root = Path(output_dir())
root.mkdir(parents=True, exist_ok=True)
with (root / ".render.lock").open("a") as lock:
try:
async with asyncio.timeout(timeout_sec):
while True:
try:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
break
except BlockingIOError:
await asyncio.sleep(0.1)
return await _run_unlocked(args, timeout_sec)
except TimeoutError:
return None, "렌더 대기 또는 실행 시간 초과"
finally:
fcntl.flock(lock, fcntl.LOCK_UN)
async def _run_unlocked(args: list[str], timeout_sec: float) -> tuple[int | None, str]:
"""렌더러 프로세스를 돌리고 (exit_code, stderr 일부) 를 돌려준다.
★ 타임아웃이면 프로세스를 죽이고 회수한다 — 좀비로 남겨 워커 컨테이너의 프로세스 표를
채우면 안 된다. exit_code 는 None 으로 남아 "시간 안에 안 끝났다" 를 구분한다."""
proc = await asyncio.create_subprocess_exec(
*args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
try:
_stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout_sec)
except (asyncio.TimeoutError, TimeoutError):
proc.kill()
# kill 후에도 반드시 wait 한다 — 안 하면 좀비 프로세스로 남는다.
await proc.wait()
return None, f"{timeout_sec:.0f}초 안에 끝나지 않아 강제 종료했다"
except asyncio.CancelledError:
if proc.returncode is None:
proc.kill()
await proc.wait()
raise
# 사유는 앞부분에 나온다(watch-payloads.mjs 의 run() 과 같은 규칙) — 스택 전체를 들 이유가 없다.
return proc.returncode, stderr.decode("utf-8", errors="replace")[:4000]
async def render_site(payload_path: str, site_version: int, timeout_sec: float) -> dict:
"""payload 파일 하나를 굽는다. 끝나면 렌더 보고서(dict)를 돌려준다.
★ 게이트 판정은 여기서 하지 않는다 — 반환된 보고서의 `ok`·`mismatches`·
`uniqueContentCount` 를 build_service.run_build 가 그대로 판단한다(예전과 같은 계약,
render_report.wait_for 가 폴링해서 얻던 것과 같은 모양이다).
★ 멱등이다 — 같은 payload 파일로 다시 부르면 같은 결과가 나온다(재시도 안전). 렌더러
자신이 사이트별로 실패를 격리하므로, 이미 성공한 사이트를 다시 구워도 결과는 같다."""
slug = Path(payload_path).stem
# 재시도에서 같은 버전의 옛 성공 보고서를 이번 실행 결과로 읽지 않는다.
render_report.report_path(slug).unlink(missing_ok=True)
args = [
os.environ.get(NODE_BIN_ENV) or DEFAULT_NODE_BIN,
renderer_entry(),
f"--payload={payload_path}",
f"--out={output_dir()}",
"--stage-only",
]
try:
code, stderr = await _run_subprocess(args, timeout_sec)
except OSError as ex:
raise RenderFailed(f"렌더러 실행 실패: {type(ex).__name__}") from ex
report = render_report.read_report(slug)
if code in (0, 1) and report is not None and report.get("siteVersion") == site_version:
return report
# ★ 프로세스가 죽거나 시간 초과였는데 보고서도 없으면(사이트를 하나도 돌기 전에 죽은 경우)
# 대신 실패로 남긴다 — 안 그러면 build_service 가 "렌더 결과를 못 받았다" 는 것만 알고
# 진짜 사유(모듈 누락·OOM 등)는 워커 로그에만 남는다.
reason = (
f"렌더러가 {timeout_sec:.0f}초 안에 끝나지 않았다"
if code is None
else f"렌더러가 비정상 종료했다(exit {code})" if code != 0 else "보고서를 남기지 못했다(버전 불일치 포함)"
)
detail = f"{reason}" + (f" — {stderr.strip()}" if stderr.strip() else "")
LOG.w(f"[render] {slug} v{site_version}: {detail}")
raise RenderFailed(detail)
async def activate_site(slug: str, site_version: int) -> None:
"""두 게이트를 통과한 뒤 Node가 공개 링크만 전환한다."""
args = [
os.environ.get(NODE_BIN_ENV) or DEFAULT_NODE_BIN,
renderer_entry(), f"--out={output_dir()}", f"--activate={slug}:{site_version}",
]
try:
code, _stderr = await _run_subprocess(args, 30)
except OSError as ex:
raise RenderFailed(f"공개 전환 실행 실패: {type(ex).__name__}") from ex
if code != 0:
raise RenderFailed("공개 버전 전환 실패")

View File

@ -0,0 +1,158 @@
"""예전 버전으로 되돌리기 — ROLLBACK 잡이 하는 일.
대상 버전 스냅샷 로드 → payload 재조립 → 렌더 → 게이트 → 공개 주소 전환
★ 재수집·재생성을 하지 않는다. run_build 와 다른 점이 이것 하나다 — 대상 버전이
박제해 둔 snapshot(site_versions.snapshot)을 **그대로** 다시 payload 로 옮길 뿐이다.
사장님이 그때 이미 확인하고 발행했던 값이므로, 지금 fact 상태가 바뀌었어도 롤백은
그 시점의 값을 그대로 복원한다 — "롤백했는데 최신 수정이 섞여 있다"를 막는다.
★ 그래도 게이트는 다시 돈다. 스냅샷은 그대로지만 **렌더러는 그때와 다를 수 있다**
(그 사이 배포됐을 수 있다) — 새 렌더러가 옛 스냅샷을 어떻게 굽는지는 다시 확인해야 한다.
★ 재굽기가 필요 없을 수도 있다 — 대상 버전 디렉토리가 보관 기간 안에 아직 디스크에 있으면
렌더러가 그 사실을 알아서 이용한다(prerender.ts publishVersion 은 렌더 자체는 매번 다시
하지만, 결과가 같으면 파일 내용도 같다 — 멱등이다). 디스크에서 지워졌어도 문제없다 —
snapshot 이 있으니 처음부터 다시 굽는다.
★ 실패하면 지금 공개된 버전은 그대로다. render_service.render_site 가 실패를 던지거나
report.ok=False 면 심볼릭 링크를 아예 안 돌린다(prerender.ts publishVersion 은 성공한
렌더 결과에만 닿는다) — 발행 파이프라인과 같은 원자성 보장이다.
"""
import os
import uuid
from common.database.db_session_manager import DB_SESSION_MNG
from common.database.model.models import places, site_publish_logs, site_versions, sites
from common.enums import (
BuildStatus,
DBWRType,
ErrorType,
PlaceCategory,
PublishAction,
PublishResult,
SiteStatus,
)
from common.logger import LOG
from common.utils.gtime import GTime
from crud.place_crud import PlaceCRUD
from crud.site_crud import SiteCRUD
from services import azure_static, indexnow, publish_gate, render_service, site_payload
from services.build_service import ensure_site, load_channel_links
_site_crud = SiteCRUD()
_place_crud = PlaceCRUD()
RENDER_TIMEOUT_SEC_ENV = "RENDER_TIMEOUT_SEC"
class RollbackAborted(RuntimeError):
"""재시도해도 소용없는 중단 — 잡의 last_error 로 남는다."""
async def _log(site_id, version_id, result: PublishResult, gate=None, actor=None):
row = site_publish_logs(
site_id=site_id,
site_version_id=version_id,
action=PublishAction.ROLLBACK.value,
result=result.value,
reject_reason=(gate.reason.value if gate is not None and gate.reason else None),
detail=(gate.as_log() if gate is not None and not gate.passed else None),
actor_user_id=uuid.UUID(actor) if actor else None,
)
await DB_SESSION_MNG.execute_lambda_run([site_publish_logs.DBType()], [lambda s: _site_crud.add_log(s, row)])
async def run_rollback(job: dict) -> dict:
"""ROLLBACK 잡 핸들러. payload: {place_id, owner_user_id, target_version, requested_by?}"""
payload = job["payload"]
place_id = payload["place_id"]
owner_user_id = payload["owner_user_id"]
target_version = int(payload["target_version"])
render_timeout_sec = float(os.environ.get(RENDER_TIMEOUT_SEC_ENV) or 180)
err, place = await DB_SESSION_MNG.execute_lambda(
places.DBType(),
DBWRType.DB_READ.value,
lambda s: _place_crud.get_place(s, uuid.UUID(owner_user_id), uuid.UUID(place_id)),
)
if err != ErrorType.SUCCESS or place is None:
raise RollbackAborted(f"사업장을 찾을 수 없다: {place_id}")
site = await ensure_site(place_id)
v_err, version = await DB_SESSION_MNG.execute_lambda(
site_versions.DBType(),
DBWRType.DB_READ.value,
lambda s: _site_crud.get_version_by_number(s, site.site_id, target_version),
)
if v_err != ErrorType.SUCCESS or version is None:
raise RollbackAborted(f"버전 {target_version} 을 찾을 수 없다")
if version.build_status != BuildStatus.BUILT.value or not version.snapshot:
raise RollbackAborted(f"버전 {target_version} 은 성공적으로 빌드된 적이 없어 되돌릴 수 없다")
if site.current_version_id == version.site_version_id:
raise RollbackAborted(f"이미 지금 공개 중인 버전이다(v{target_version})")
result = {
"place_id": place_id, "site_id": str(site.site_id),
"target_version": target_version, "site_version_id": str(version.site_version_id),
}
async def _fail(reason: str, gate: publish_gate.GateResult | None = None):
await _log(site.site_id, version.site_version_id, PublishResult.REJECTED if gate else PublishResult.FAILED,
gate, payload.get("requested_by"))
result["rolled_back"] = False
result["error"] = reason
LOG.w(f"[rollback] place={place_id} v{target_version} 실패: {reason}")
return result
# ★ snapshot 을 그대로 옮긴다 — 재수집하지 않는다(파일 머리주석 참조).
links = await load_channel_links(place_id)
payload_dict = await site_payload.prepare_site_payload(
place, version.snapshot, site, version, links, publish=True
)
payload_path = site_payload.write_payload(payload_dict)
result["payload_path"] = payload_path
try:
report = await render_service.render_site(payload_path, target_version, render_timeout_sec)
except render_service.RenderFailed as ex:
return await _fail(str(ex))
mismatches = list(report.get("mismatches") or [])
unique_count_raw = report.get("uniqueContentCount")
# ★ 렌더러가 그때와 달라졌을 수 있으므로 게이트를 다시 돈다(파일 머리주석 참조) —
# fact 검증 게이트(1차)는 건너뛴다. snapshot 은 이미 확인된 값만 담아 발행했던 것이라
# 다시 물을 것이 없다 — 여기서 또 물으면 "그때는 됐는데 지금은 안 된다"는 모순이 난다.
gate = publish_gate.evaluate(
PlaceCategory(place.category), version.snapshot.get("facts") or [], unique_count_raw, mismatches
)
result["gate"] = {"passed": gate.passed, **gate.as_log()}
if not gate.passed:
return await _fail(f"{gate.reason.name}: {gate.as_log()}", gate)
if not report.get("ok"):
return await _fail(str(report.get("error") or "렌더 실패"))
slug = site_payload.publish_slug(place, site)
try:
await render_service.activate_site(slug, target_version)
await azure_static.publish(slug)
except (render_service.RenderFailed, azure_static.AzurePublishError) as ex:
return await _fail(str(ex))
await indexnow.submit(slug)
now = GTime.UTC()
site_update = {
"status": SiteStatus.PUBLISHED.value,
"current_version_id": version.site_version_id,
"published_at": now,
}
await DB_SESSION_MNG.execute_lambda_claim(
sites.DBType(), lambda s: _site_crud.update_site(s, site.site_id, site_update)
)
await _log(site.site_id, version.site_version_id, PublishResult.SUCCESS, None, payload.get("requested_by"))
result["rolled_back"] = True
LOG.i(f"[rollback] place={place_id} v{target_version} 로 되돌림")
return result

View File

@ -39,8 +39,13 @@ from services.stay_guide import nol_stay_guide
SCHEMA_VERSION = 1 SCHEMA_VERSION = 1
# 출력 디렉토리. 컨테이너 밖(볼륨·오브젝트 스토리지)으로 빼기 쉬우라고 env 로 둔다. # 출력 디렉토리. 컨테이너 밖(볼륨·오브젝트 스토리지)으로 빼기 쉬우라고 env 로 둔다.
# ★ 기본값이 `solution/site/payloads` 아래인 이유 — 렌더러(prerender.ts)가 songs·out 디렉토리를
# **자기 파일 위치 기준 상대경로**로 찾는다(SITE_ROOT = dist/prerender/../..). 워커가 그
# 렌더러를 subprocess 로 직접 띄우면서(render_service.py) 세 디렉토리(payloads·songs·out)가
# 그 렌더러가 실제로 설치된 자리(`/app/solution/site/`) 아래에 나란히 있어야 한다 —
# 어긋나면 워커는 payload 를 잘 쓰는데 렌더러는 다른 곳에서 songs 를 찾다가 못 찾는다.
PAYLOAD_DIR_ENV = "SITE_PAYLOAD_DIR" PAYLOAD_DIR_ENV = "SITE_PAYLOAD_DIR"
DEFAULT_PAYLOAD_DIR = "/app/out/payloads" DEFAULT_PAYLOAD_DIR = "/app/solution/site/payloads"
# 커스텀 도메인이 없을 때 쓰는 기본 호스트. sites.domain 이 채워지면 그 값이 이긴다. # 커스텀 도메인이 없을 때 쓰는 기본 호스트. sites.domain 이 채워지면 그 값이 이긴다.
# #
@ -780,11 +785,15 @@ def region_label(*addresses: str | None) -> str | None:
# ── payload 조립 ────────────────────────────────────────────────────────── # ── payload 조립 ──────────────────────────────────────────────────────────
def to_site_payload(place, snapshot: dict, site, version, links) -> dict: def to_site_payload(place, snapshot: dict, site, version, links, publish: bool = False) -> dict:
"""스냅샷 + 사이트/버전 행 + 채널 링크 → SitePayload(dict). """스냅샷 + 사이트/버전 행 + 채널 링크 → SitePayload(dict).
순수 변환 함수다. DB 도 파일도 건드리지 않는다 — 그래야 테스트가 쉽고, 순수 변환 함수다. DB 도 파일도 건드리지 않는다 — 그래야 테스트가 쉽고,
같은 입력이면 언제나 같은 payload 가 나온다.""" 같은 입력이면 언제나 같은 payload 가 나온다.
★ `publish` 는 렌더러(prerender.ts publishVersion)에게 "이 버전으로 공개 주소를
넘겨도 되는가"를 알리는 신호다. False(미리보기·게이트 통과 전 재빌드)면 렌더러가
`out/versions/<slug>/<version>/` 에만 굽고 `out/s/<slug>` 심볼릭 링크는 그대로 둔다."""
snapshot = snapshot or {} snapshot = snapshot or {}
snap_place = snapshot.get("place") or {} snap_place = snapshot.get("place") or {}
place_id = str(_get(place, "place_id")) place_id = str(_get(place, "place_id"))
@ -929,6 +938,7 @@ def to_site_payload(place, snapshot: dict, site, version, links) -> dict:
"publishedAt": published_at or updated_at, "publishedAt": published_at or updated_at,
"updatedAt": updated_at, "updatedAt": updated_at,
"version": int(_get(version, "version") or 1), "version": int(_get(version, "version") or 1),
"publish": bool(publish),
}, },
"place": { "place": {
"name": name, "name": name,
@ -1037,9 +1047,9 @@ def write_payload(payload: dict) -> str:
return str(path) return str(path)
async def prepare_site_payload(place, snapshot: dict, site, version, links) -> dict: async def prepare_site_payload(place, snapshot: dict, site, version, links, publish: bool = False) -> dict:
"""미리보기·발행 공통 보강. 요약은 DB 스냅샷이 아니라 응답/산출물에만 싣는다.""" """미리보기·발행 공통 보강. 요약은 DB 스냅샷이 아니라 응답/산출물에만 싣는다."""
payload = to_site_payload(place, snapshot, site, version, links) payload = to_site_payload(place, snapshot, site, version, links, publish)
for fact in payload["facts"]: for fact in payload["facts"]:
if fact["key"] != "intro" or fact["status"] not in {s.value for s in PUBLISHABLE_FACT_STATUSES}: if fact["key"] != "intro" or fact["status"] not in {s.value for s in PUBLISHABLE_FACT_STATUSES}:
continue continue
@ -1054,14 +1064,14 @@ async def prepare_site_payload(place, snapshot: dict, site, version, links) -> d
return payload return payload
async def emit_payload(place, snapshot: dict, site, version, links) -> str | None: async def emit_payload(place, snapshot: dict, site, version, links, publish: bool = False) -> str | None:
"""payload 조립 + 파일 쓰기. 실패해도 예외를 밖으로 내보내지 않는다. """payload 조립 + 파일 쓰기. 실패해도 예외를 밖으로 내보내지 않는다.
★ 발행 자체를 실패시키면 안 된다 — 게이트를 통과해 DB 에 남은 발행 기록은 이미 정확하고, ★ 발행 자체를 실패시키면 안 된다 — 게이트를 통과해 DB 에 남은 발행 기록은 이미 정확하고,
payload 는 그것을 화면으로 옮기는 부수 산출물이다. 디스크가 없거나 권한이 없어서 payload 는 그것을 화면으로 옮기는 부수 산출물이다. 디스크가 없거나 권한이 없어서
발행이 되돌려지는 게 더 나쁘다. 대신 경고 로그로 반드시 드러낸다.""" 발행이 되돌려지는 게 더 나쁘다. 대신 경고 로그로 반드시 드러낸다."""
try: try:
payload = await prepare_site_payload(place, snapshot, site, version, links) payload = await prepare_site_payload(place, snapshot, site, version, links, publish)
path = write_payload(payload) path = write_payload(payload)
LOG.i( LOG.i(
f"[payload] {payload['site']['slug']} → {path} " f"[payload] {payload['site']['slug']} → {path} "

View File

@ -29,6 +29,7 @@ from router.v1.site.protocol import (
Req_SiteStatus, Req_SiteStatus,
Req_SiteTemplate, Req_SiteTemplate,
Req_SiteTheme, Req_SiteTheme,
Req_Rollback,
Req_StartBuild, Req_StartBuild,
Res_MySites, Res_MySites,
Res_PublishLogs, Res_PublishLogs,
@ -166,7 +167,7 @@ class SiteService:
(`snapshot.build_snapshot` → `site_payload.prepare_site_payload`)를 그대로 거치므로, (`snapshot.build_snapshot` → `site_payload.prepare_site_payload`)를 그대로 거치므로,
여기서 갈릴 자리가 없다. 버전은 아직 없으니 0 으로 넘긴다 — 화면에 안 쓰인다. 여기서 갈릴 자리가 없다. 버전은 아직 없으니 0 으로 넘긴다 — 화면에 안 쓰인다.
""" """
from services.build_service import ensure_site, _load_links from services.build_service import ensure_site, load_channel_links
from services.site_payload import prepare_site_payload from services.site_payload import prepare_site_payload
from services.snapshot import build_snapshot from services.snapshot import build_snapshot
@ -175,7 +176,7 @@ class SiteService:
return None return None
site = await ensure_site(place_id) site = await ensure_site(place_id)
links = await _load_links(place_id) links = await load_channel_links(place_id)
snapshot = await build_snapshot(place) snapshot = await build_snapshot(place)
# ★ version 은 None 이다. 발행 전이라 버전 행이 없고, payload 의 site.version 은 # ★ version 은 None 이다. 발행 전이라 버전 행이 없고, payload 의 site.version 은
# 캐시 무효화 키라 미리보기에서는 뜻이 없다(to_site_payload 가 0 으로 떨어뜨린다). # 캐시 무효화 키라 미리보기에서는 뜻이 없다(to_site_payload 가 0 으로 떨어뜨린다).
@ -570,6 +571,35 @@ class SiteService:
res.created = created res.created = created
return res return res
async def start_rollback(self, user_info: UserInfo, place_id: str, req: Req_Rollback) -> Res_StartBuild:
"""예전 버전으로 공개 주소를 되돌리는 잡을 큐에 넣는다.
★ dedupe_key 를 빌드와 **같은 것**(`build:{place_id}`)을 쓴다 — 롤백 도중에 새 발행이
끼어들거나 그 반대가 되면 어느 쪽이 이겼는지 알 수 없는 상태가 된다. 이 사이트의
공개 주소를 바꾸는 작업은 항상 하나만 활성화된다."""
res = Res_StartBuild()
err_type, _place = await self._load_place(user_info, place_id)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
job_id, created = await enqueue_job(
self.queue, JobType.ROLLBACK,
{
"place_id": place_id, "owner_user_id": user_info.user_id,
"target_version": req.target_version, "requested_by": user_info.user_id,
},
dedupe_key=f"build:{place_id}",
)
if job_id is None:
res.result.SetResult(ErrorType.COLLECT_ALREADY_RUNNING)
return res
res.job_id = uuid.UUID(job_id)
res.status = JobStatus.PENDING
res.created = created
return res
async def list_versions(self, user_info: UserInfo, place_id: str) -> Res_SiteVersions: async def list_versions(self, user_info: UserInfo, place_id: str) -> Res_SiteVersions:
from sqlalchemy import select from sqlalchemy import select

View File

@ -64,7 +64,7 @@ async def _generate_kind(client: httpx.AsyncClient, kind: str, region_label: str
{"role": "system", "content": prompts.SYSTEM_PROMPT}, {"role": "system", "content": prompts.SYSTEM_PROMPT},
{"role": "user", "content": prompts.build_prompt(kind, region_label)}, {"role": "user", "content": prompts.build_prompt(kind, region_label)},
], ],
"max_tokens": perplexity.DEFAULT_MAX_TOKENS, "max_tokens": _MAX_TOKENS.get(kind, perplexity.DEFAULT_MAX_TOKENS),
} }
try: try:
payload = await perplexity.call(body, client=client) payload = await perplexity.call(body, client=client)
@ -80,6 +80,15 @@ async def _generate_kind(client: httpx.AsyncClient, kind: str, region_label: str
return items, dropped return items, dropped
# 종류별 응답 토큰 상한. 기본(2048)으로 모자란 종류만 적는다.
#
# ★ 왜 (실측 2026-09-15) `reading` 은 30~34꼭지 × 서너 문장이라 다른 종류의 서너 배다.
# 2048 로 부르면 응답이 **문장 한가운데서 잘려** 오고, 파서는 그걸 "JSON 이 아니다" 로
# 통째로 버린다 — 여섯 지역 전부 0건이었다. 잘린 것은 재시도해도 같은 자리에서 잘린다.
# ★ 상한만 올린다. 꼭지 수를 줄이면 화면이 매번 5~6개만 뽑는 의미가 없어진다
# (`site/sections/items/ReadingSection.tsx`).
_MAX_TOKENS = {"reading": 8000}
# 사진을 무엇으로 찾을지. 종류마다 출처가 다르다. # 사진을 무엇으로 찾을지. 종류마다 출처가 다르다.
# #
# chronicle · postcard 공공데이터(TourAPI) — 장소·시설 사진. 공공누리 Type1/Type3 만 # chronicle · postcard 공공데이터(TourAPI) — 장소·시설 사진. 공공누리 Type1/Type3 만

View File

@ -100,10 +100,10 @@ async def test_unverified_fact_blocks_publish(auth_headers, client, db_engine):
(필수 항목 누락은 2026-08-27 부터 막지 않는다. 그래서 여기서 걸리는 사유가 바뀌었다.)""" (필수 항목 누락은 2026-08-27 부터 막지 않는다. 그래서 여기서 걸리는 사유가 바뀌었다.)"""
h = await auth_headers("u1") h = await auth_headers("u1")
pid = await _place(client, h, "미검증펜션") pid = await _place(client, h, "미검증펜션")
# 크롤링으로만 들어온 값 = 후보 상태 # 크롤링은 바로 노출되는 정책이다. 실제 미검증 상태를 만드는 API 출처로 검사한다.
for k, v in REQUIRED.items(): for k, v in REQUIRED.items():
await client.post(f"/v1/place/{pid}/fact", headers=h, json={ await client.post(f"/v1/place/{pid}/fact", headers=h, json={
"key": k, "value": v, "source_type": SourceType.CRAWL.value, "source_url": "https://ota.test/1"}) "key": k, "value": v, "source_type": SourceType.API.value, "source_url": "https://api.test/1"})
job_id = (await client.post(f"/v1/place/{pid}/site/build", headers=h, json={"publish": True})).json()["job_id"] job_id = (await client.post(f"/v1/place/{pid}/site/build", headers=h, json={"publish": True})).json()["job_id"]
await _run() await _run()
@ -292,17 +292,17 @@ async def _build(client, h, pid, publish=True):
async def test_렌더가_실패하면_발행하지_않는다(auth_headers, client, db_engine, monkeypatch): async def test_렌더가_실패하면_발행하지_않는다(auth_headers, client, db_engine, monkeypatch):
"""검증: 렌더러가 페이지를 굽지 못했다고 보고한다. """검증: 렌더러가 페이지를 굽지 못했다고 보고한다.
기대결과: ★ FAILED. 페이지가 없는데 "발행됨"으로 남으면 사장님은 404 를 눌러야 안다.""" 기대결과: ★ FAILED. 페이지가 없는데 "발행됨"으로 남으면 사장님은 404 를 눌러야 안다."""
from services import render_report from services import render_service
async def _broken(slug, site_version, timeout_sec, poll_sec=1.0): async def _broken(payload_path, site_version, timeout_sec):
return {"schemaVersion": 1, "slug": slug, "siteId": "", "placeId": "", return {"schemaVersion": 1, "slug": "t", "siteId": "", "placeId": "",
"siteVersion": site_version, "ok": False, "renderedAt": "2026-01-01T00:00:00Z", "siteVersion": site_version, "ok": False, "renderedAt": "2026-01-01T00:00:00Z",
"routes": 0, "bundle": "t.js", "uniqueContentCount": 5, "jsonld": [], "routes": 0, "bundle": "t.js", "uniqueContentCount": 5, "jsonld": [],
"mismatches": [], "error": "번들이 없다"} "mismatches": [], "error": "번들이 없다"}
h = await auth_headers("u1") h = await auth_headers("u1")
pid = await _built_place(client, h, db_engine) pid = await _built_place(client, h, db_engine)
monkeypatch.setattr(render_report, "wait_for", _broken) monkeypatch.setattr(render_service, "render_site", _broken)
r = await _build(client, h, pid) r = await _build(client, h, pid)
assert r["build_status"] == "FAILED" assert r["build_status"] == "FAILED"
@ -316,10 +316,10 @@ async def test_렌더가_실패하면_발행하지_않는다(auth_headers, clien
async def test_구조화데이터가_화면과_다르면_발행하지_않는다(auth_headers, client, db_engine, monkeypatch): async def test_구조화데이터가_화면과_다르면_발행하지_않는다(auth_headers, client, db_engine, monkeypatch):
"""검증: 렌더러가 JSON-LD 불일치를 보고한다. """검증: 렌더러가 JSON-LD 불일치를 보고한다.
기대결과: ★ JSONLD_MISMATCH — 검색엔진에만 다른 말을 하는 페이지는 나가면 안 된다.""" 기대결과: ★ JSONLD_MISMATCH — 검색엔진에만 다른 말을 하는 페이지는 나가면 안 된다."""
from services import render_report from services import render_service
async def _mismatch(slug, site_version, timeout_sec, poll_sec=1.0): async def _mismatch(payload_path, site_version, timeout_sec):
return {"schemaVersion": 1, "slug": slug, "siteId": "", "placeId": "", return {"schemaVersion": 1, "slug": "t", "siteId": "", "placeId": "",
"siteVersion": site_version, "ok": False, "renderedAt": "2026-01-01T00:00:00Z", "siteVersion": site_version, "ok": False, "renderedAt": "2026-01-01T00:00:00Z",
"routes": 0, "bundle": "t.js", "uniqueContentCount": 5, "jsonld": [], "routes": 0, "bundle": "t.js", "uniqueContentCount": 5, "jsonld": [],
"mismatches": ["telephone: '02-0000-0000' 이 화면에 없다"], "mismatches": ["telephone: '02-0000-0000' 이 화면에 없다"],
@ -327,7 +327,7 @@ async def test_구조화데이터가_화면과_다르면_발행하지_않는다(
h = await auth_headers("u1") h = await auth_headers("u1")
pid = await _built_place(client, h, db_engine) pid = await _built_place(client, h, db_engine)
monkeypatch.setattr(render_report, "wait_for", _mismatch) monkeypatch.setattr(render_service, "render_site", _mismatch)
r = await _build(client, h, pid) r = await _build(client, h, pid)
assert r["build_status"] == "FAILED" assert r["build_status"] == "FAILED"
@ -339,14 +339,14 @@ async def test_구조화데이터가_화면과_다르면_발행하지_않는다(
async def test_렌더_결과가_안_오면_발행하지_않는다(auth_headers, client, db_engine, monkeypatch): async def test_렌더_결과가_안_오면_발행하지_않는다(auth_headers, client, db_engine, monkeypatch):
"""검증: 렌더러가 죽어 보고서가 오지 않는다(타임아웃). """검증: 렌더러가 죽어 보고서가 오지 않는다(타임아웃).
기대결과: ★ FAILED. 확인하지 못한 것을 발행됨으로 남기지 않는다.""" 기대결과: ★ FAILED. 확인하지 못한 것을 발행됨으로 남기지 않는다."""
from services import render_report from services import render_service
async def _timeout(slug, site_version, timeout_sec, poll_sec=1.0): async def _timeout(payload_path, site_version, timeout_sec):
return None raise render_service.RenderFailed("렌더 결과를 받지 못했다 — 렌더러가 도는지 확인이 필요하다")
h = await auth_headers("u1") h = await auth_headers("u1")
pid = await _built_place(client, h, db_engine) pid = await _built_place(client, h, db_engine)
monkeypatch.setattr(render_report, "wait_for", _timeout) monkeypatch.setattr(render_service, "render_site", _timeout)
r = await _build(client, h, pid) r = await _build(client, h, pid)
assert r["build_status"] == "FAILED" assert r["build_status"] == "FAILED"

View File

@ -0,0 +1,38 @@
import asyncio
import sys
import pytest
from services import render_service
@pytest.fixture(autouse=True)
def fake_renderer(monkeypatch, tmp_path):
# 이 파일은 conftest의 렌더 대역 대신 실제 자식 프로세스 수명만 검사한다.
monkeypatch.setenv("SITE_OUTPUT_DIR", str(tmp_path))
async def test_process_success():
code, _ = await render_service._run_subprocess([sys.executable, "-c", "print(1)"], 2)
assert code == 0
async def test_timeout_releases_lock():
code, _ = await render_service._run_subprocess(
[sys.executable, "-c", "import time;time.sleep(5)"], 0.1
)
assert code is None
code, _ = await render_service._run_subprocess([sys.executable, "-c", "print(1)"], 2)
assert code == 0
async def test_cancel_releases_lock():
task = asyncio.create_task(render_service._run_subprocess(
[sys.executable, "-c", "import time;time.sleep(5)"], 10
))
await asyncio.sleep(0.1)
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
code, _ = await render_service._run_subprocess([sys.executable, "-c", "print(1)"], 2)
assert code == 0

View File

@ -0,0 +1,131 @@
"""롤백 — 예전 버전으로 공개 주소를 되돌린다.
★ 이 경로가 절대 하면 안 되는 것:
- 재수집·재생성으로 스냅샷을 새로 만드는 것 (그 버전이 발행됐던 그대로여야 한다)
- 실패했는데 site.current_version_id 를 바꾸는 것 (직전 공개본이 그대로 있어야 한다)
- 지금 공개 중인 버전으로 "롤백"을 허용하는 것 (의미 없는 잡을 만든다)
"""
import uuid
from common.enums import BuildStatus, JobStatus, SiteStatus
from crud.job_crud import JobQueue
from worker.handlers import build_handler
from worker.runner import Worker
from tests.test_build_publish import _approved_media, _place, _verified_facts
async def _run():
assert await Worker("w", JobQueue(), build_handler(), job_deadline_sec=60).process_one() is True
async def _publish(client, h, pid, intro="하조대 해변 도보 3분 거리의 펜션입니다."):
await _verified_facts(client, h, pid, {"intro": intro})
job_id = (await client.post(f"/v1/place/{pid}/site/build", headers=h, json={"publish": True})).json()["job_id"]
await _run()
return (await client.get(f"/v1/job/{job_id}", headers=h)).json()["job"]["result"]
async def test_rollback_to_earlier_version(auth_headers, client, db_engine):
"""검증: v1 발행 → v2 발행 → v1 로 롤백.
기대결과: 공개 버전이 다시 v1 이 된다. 롤백은 새 site_versions 행을 만들지 않는다."""
h = await auth_headers("u1")
pid = await _place(client, h)
await _approved_media(db_engine, pid)
r1 = await _publish(client, h, pid, "v1 소개 — 하조대 해변 도보 3분 거리의 펜션입니다.")
assert r1["published"] is True
r2 = await _publish(client, h, pid, "v2 소개 — 리모델링을 마친 하조대 앞 오션뷰 펜션입니다.")
assert r2["published"] is True
assert r2["version"] == r1["version"] + 1
job_id = (await client.post(
f"/v1/place/{pid}/site/version/rollback", headers=h, json={"target_version": r1["version"]}
)).json()["job_id"]
await _run()
job = (await client.get(f"/v1/job/{job_id}", headers=h)).json()["job"]
assert job["status"] == JobStatus.DONE.value, job.get("last_error")
result = job["result"]
assert result["rolled_back"] is True, result.get("error")
assert result["target_version"] == r1["version"]
site = (await client.get(f"/v1/place/{pid}/site", headers=h)).json()
assert site["site"]["status"] == SiteStatus.PUBLISHED.value
assert site["current_version"]["version"] == r1["version"]
assert site["current_version"]["build_status"] == BuildStatus.BUILT.value
versions = (await client.get(f"/v1/place/{pid}/site/version/list", headers=h)).json()["versions"]
# ★ 롤백은 새 버전을 만들지 않는다 — v1·v2 두 개뿐이어야 한다.
assert len(versions) == 2
async def test_rollback_rejects_current_version(auth_headers, client, db_engine):
"""검증: 지금 공개 중인 버전으로 롤백을 시도한다.
기대결과: ★ 성공으로 끝나지 않는다(last_error) — 의미 없는 재굽기를 하지 않는다.
(BuildAborted 와 같은 패턴 — 재시도해도 소용없는 중단이라 PENDING(백오프)으로 남고,
소진되면 DEAD 된다. 여기서 보는 건 "그래도 site 상태는 안 바뀐다"는 것이다.)"""
h = await auth_headers("u1")
pid = await _place(client, h)
await _approved_media(db_engine, pid)
r1 = await _publish(client, h, pid)
job_id = (await client.post(
f"/v1/place/{pid}/site/version/rollback", headers=h, json={"target_version": r1["version"]}
)).json()["job_id"]
await _run()
job = (await client.get(f"/v1/job/{job_id}", headers=h)).json()["job"]
assert job["status"] != JobStatus.DONE.value
assert "이미" in (job.get("last_error") or "")
site = (await client.get(f"/v1/place/{pid}/site", headers=h)).json()
assert site["current_version"]["version"] == r1["version"]
async def test_rollback_rejects_unknown_version(auth_headers, client, db_engine):
"""검증: 존재하지 않는 버전 번호로 롤백을 시도한다.
기대결과: ★ 잡이 성공으로 끝나지 않는다 — 공개 상태는 그대로다."""
h = await auth_headers("u1")
pid = await _place(client, h)
await _approved_media(db_engine, pid)
r1 = await _publish(client, h, pid)
job_id = (await client.post(
f"/v1/place/{pid}/site/version/rollback", headers=h, json={"target_version": 999}
)).json()["job_id"]
await _run()
job = (await client.get(f"/v1/job/{job_id}", headers=h)).json()["job"]
assert job["status"] != JobStatus.DONE.value
site = (await client.get(f"/v1/place/{pid}/site", headers=h)).json()
assert site["site"]["status"] == SiteStatus.PUBLISHED.value
assert site["current_version"]["version"] == r1["version"]
async def test_failed_rollback_keeps_current_published_version(auth_headers, client, db_engine, monkeypatch):
"""검증: 롤백 대상 렌더가 실패한다(구조화 데이터 불일치로 시뮬레이션).
기대결과: ★ site.current_version_id 는 바뀌지 않는다 — 직전 공개본이 그대로 서비스된다."""
from services import render_service
h = await auth_headers("u1")
pid = await _place(client, h)
await _approved_media(db_engine, pid)
r1 = await _publish(client, h, pid, "v1 소개 — 하조대 해변 도보 3분 거리의 펜션입니다.")
r2 = await _publish(client, h, pid, "v2 소개 — 리모델링을 마친 하조대 앞 오션뷰 펜션입니다.")
site_before = (await client.get(f"/v1/place/{pid}/site", headers=h)).json()
assert site_before["current_version"]["version"] == r2["version"]
async def _broken(payload_path, site_version, timeout_sec):
raise render_service.RenderFailed("렌더러가 죽었다(시뮬레이션)")
monkeypatch.setattr(render_service, "render_site", _broken)
job_id = (await client.post(
f"/v1/place/{pid}/site/version/rollback", headers=h, json={"target_version": r1["version"]}
)).json()["job_id"]
await _run()
job = (await client.get(f"/v1/job/{job_id}", headers=h)).json()["job"]
assert job["status"] == JobStatus.DONE.value
assert job["result"]["rolled_back"] is False
site_after = (await client.get(f"/v1/place/{pid}/site", headers=h)).json()
assert site_after["current_version"]["version"] == r2["version"], "실패했는데 공개 버전이 바뀌었다"

View File

@ -16,6 +16,7 @@
JobType.BUILD ✓ services/build_service.run_build — 정적 빌드 + 발행 검수 게이트 JobType.BUILD ✓ services/build_service.run_build — 정적 빌드 + 발행 검수 게이트
JobType.LOCAL_SYNC ✓ services/story_service.run_local_sync — 지역 이야기 생성(지역당 1회) JobType.LOCAL_SYNC ✓ services/story_service.run_local_sync — 지역 이야기 생성(지역당 1회)
JobType.SONG ✓ services/song_service.run_song — 이 숙소의 노래 한 곡(가사 Gemini → 작곡 Suno) JobType.SONG ✓ services/song_service.run_song — 이 숙소의 노래 한 곡(가사 Gemini → 작곡 Suno)
JobType.ROLLBACK ✓ services/rollback_service.run_rollback — 예전 버전으로 공개 주소를 되돌림
JobType.AI_CHECK → reports 모듈이 붙을 때 JobType.AI_CHECK → reports 모듈이 붙을 때
""" """
@ -82,6 +83,7 @@ def _register_builtin():
from services.copy_service import run_copy from services.copy_service import run_copy
from services.story_service import run_local_sync from services.story_service import run_local_sync
from services.song_service import run_song from services.song_service import run_song
from services.rollback_service import run_rollback
from services.vision_service import run_vision from services.vision_service import run_vision
if JobType.COLLECT.value not in HANDLERS: if JobType.COLLECT.value not in HANDLERS:
@ -96,6 +98,8 @@ def _register_builtin():
HANDLERS[JobType.LOCAL_SYNC.value] = run_local_sync HANDLERS[JobType.LOCAL_SYNC.value] = run_local_sync
if JobType.SONG.value not in HANDLERS: if JobType.SONG.value not in HANDLERS:
HANDLERS[JobType.SONG.value] = run_song HANDLERS[JobType.SONG.value] = run_song
if JobType.ROLLBACK.value not in HANDLERS:
HANDLERS[JobType.ROLLBACK.value] = run_rollback
_register_builtin() _register_builtin()

View File

@ -4,10 +4,7 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import { import { useMutation, useQuery } from "@tanstack/react-query";
useMutation,
useQuery
} from '@tanstack/react-query';
import type { import type {
DataTag, DataTag,
DefinedInitialDataOptions, DefinedInitialDataOptions,
@ -20,8 +17,8 @@ import type {
UseMutationOptions, UseMutationOptions,
UseMutationResult, UseMutationResult,
UseQueryOptions, UseQueryOptions,
UseQueryResult UseQueryResult,
} from '@tanstack/react-query'; } from "@tanstack/react-query";
import type { import type {
HTTPValidationError, HTTPValidationError,
@ -31,429 +28,576 @@ import type {
ReqUpdateMe, ReqUpdateMe,
ResLogin, ResLogin,
ResMe, ResMe,
ResRefreshToken ResRefreshToken,
} from '.././model'; } from ".././model";
import { customFetch } from '../../mutator/custom-fetch';
import { customFetch } from "../../mutator/custom-fetch";
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1]; type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/** /**
* id/pw 로 로그인하고 JWT 토큰을 발급한다. * id/pw 로 로그인하고 JWT 토큰을 발급한다.
* @summary 로그인 * @summary 로그인
*/ */
export const login = ( export const login = (
reqLogin: ReqLogin, reqLogin: ReqLogin,
options?: SecondParameter<typeof customFetch>,signal?: AbortSignal options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => { ) => {
return customFetch<ResLogin>(
{
return customFetch<ResLogin>( url: `/v1/auth/login`,
{url: `/v1/auth/login`, method: 'POST', method: "POST",
headers: {'Content-Type': 'application/json', }, headers: { "Content-Type": "application/json" },
data: reqLogin, signal data: reqLogin,
signal,
}, },
options); options,
} );
};
export const getLoginMutationOptions = <
TError = void | HTTPValidationError,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof login>>,
TError,
{ data: ReqLogin },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof login>>,
TError,
{ data: ReqLogin },
TContext
> => {
const mutationKey = ["login"];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
export const getLoginMutationOptions = <TError = void | HTTPValidationError, const mutationFn: MutationFunction<
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof login>>, TError,{data: ReqLogin}, TContext>, request?: SecondParameter<typeof customFetch>} Awaited<ReturnType<typeof login>>,
): UseMutationOptions<Awaited<ReturnType<typeof login>>, TError,{data: ReqLogin}, TContext> => { { data: ReqLogin }
> = (props) => {
const { data } = props ?? {};
const mutationKey = ['login']; return login(data, requestOptions);
const {mutation: mutationOptions, request: requestOptions} = options ? };
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
return { mutationFn, ...mutationOptions };
};
export type LoginMutationResult = NonNullable<
Awaited<ReturnType<typeof login>>
>;
export type LoginMutationBody = ReqLogin;
export type LoginMutationError = void | HTTPValidationError;
const mutationFn: MutationFunction<Awaited<ReturnType<typeof login>>, {data: ReqLogin}> = (props) => { /**
const {data} = props ?? {};
return login(data,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type LoginMutationResult = NonNullable<Awaited<ReturnType<typeof login>>>
export type LoginMutationBody = ReqLogin
export type LoginMutationError = void | HTTPValidationError
/**
* @summary 로그인 * @summary 로그인
*/ */
export const useLogin = <TError = void | HTTPValidationError, export const useLogin = <
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof login>>, TError,{data: ReqLogin}, TContext>, request?: SecondParameter<typeof customFetch>} TError = void | HTTPValidationError,
, queryClient?: QueryClient): UseMutationResult< TContext = unknown,
Awaited<ReturnType<typeof login>>, >(
TError, options?: {
{data: ReqLogin}, mutation?: UseMutationOptions<
TContext Awaited<ReturnType<typeof login>>,
> => { TError,
{ data: ReqLogin },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof login>>,
TError,
{ data: ReqLogin },
TContext
> => {
const mutationOptions = getLoginMutationOptions(options);
const mutationOptions = getLoginMutationOptions(options); return useMutation(mutationOptions, queryClient);
};
return useMutation(mutationOptions, queryClient); /**
}
/**
* id/pw 로 가입한다. 회사(테넌트) 1개가 함께 생기고, 성공하면 곧바로 로그인 토큰을 발급한다. * id/pw 로 가입한다. 회사(테넌트) 1개가 함께 생기고, 성공하면 곧바로 로그인 토큰을 발급한다.
* @summary 회원가입 * @summary 회원가입
*/ */
export const signup = ( export const signup = (
reqSignup: ReqSignup, reqSignup: ReqSignup,
options?: SecondParameter<typeof customFetch>,signal?: AbortSignal options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => { ) => {
return customFetch<ResLogin>(
{
return customFetch<ResLogin>( url: `/v1/auth/signup`,
{url: `/v1/auth/signup`, method: 'POST', method: "POST",
headers: {'Content-Type': 'application/json', }, headers: { "Content-Type": "application/json" },
data: reqSignup, signal data: reqSignup,
signal,
}, },
options); options,
} );
};
export const getSignupMutationOptions = <
TError = void | HTTPValidationError,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof signup>>,
TError,
{ data: ReqSignup },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof signup>>,
TError,
{ data: ReqSignup },
TContext
> => {
const mutationKey = ["signup"];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
export const getSignupMutationOptions = <TError = void | HTTPValidationError, const mutationFn: MutationFunction<
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof signup>>, TError,{data: ReqSignup}, TContext>, request?: SecondParameter<typeof customFetch>} Awaited<ReturnType<typeof signup>>,
): UseMutationOptions<Awaited<ReturnType<typeof signup>>, TError,{data: ReqSignup}, TContext> => { { data: ReqSignup }
> = (props) => {
const { data } = props ?? {};
const mutationKey = ['signup']; return signup(data, requestOptions);
const {mutation: mutationOptions, request: requestOptions} = options ? };
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
return { mutationFn, ...mutationOptions };
};
export type SignupMutationResult = NonNullable<
Awaited<ReturnType<typeof signup>>
>;
export type SignupMutationBody = ReqSignup;
export type SignupMutationError = void | HTTPValidationError;
const mutationFn: MutationFunction<Awaited<ReturnType<typeof signup>>, {data: ReqSignup}> = (props) => { /**
const {data} = props ?? {};
return signup(data,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type SignupMutationResult = NonNullable<Awaited<ReturnType<typeof signup>>>
export type SignupMutationBody = ReqSignup
export type SignupMutationError = void | HTTPValidationError
/**
* @summary 회원가입 * @summary 회원가입
*/ */
export const useSignup = <TError = void | HTTPValidationError, export const useSignup = <
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof signup>>, TError,{data: ReqSignup}, TContext>, request?: SecondParameter<typeof customFetch>} TError = void | HTTPValidationError,
, queryClient?: QueryClient): UseMutationResult< TContext = unknown,
Awaited<ReturnType<typeof signup>>, >(
TError, options?: {
{data: ReqSignup}, mutation?: UseMutationOptions<
TContext Awaited<ReturnType<typeof signup>>,
> => { TError,
{ data: ReqSignup },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof signup>>,
TError,
{ data: ReqSignup },
TContext
> => {
const mutationOptions = getSignupMutationOptions(options);
const mutationOptions = getSignupMutationOptions(options); return useMutation(mutationOptions, queryClient);
};
return useMutation(mutationOptions, queryClient); /**
}
/**
* 구글 ID 토큰(GIS credential)을 검증하고 JWT 토큰을 발급한다. 처음 온 계정은 그 자리에서 만든다. * 구글 ID 토큰(GIS credential)을 검증하고 JWT 토큰을 발급한다. 처음 온 계정은 그 자리에서 만든다.
* @summary 구글 로그인 * @summary 구글 로그인
*/ */
export const googleLogin = ( export const googleLogin = (
reqGoogleLogin: ReqGoogleLogin, reqGoogleLogin: ReqGoogleLogin,
options?: SecondParameter<typeof customFetch>,signal?: AbortSignal options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => { ) => {
return customFetch<ResLogin>(
{
return customFetch<ResLogin>( url: `/v1/auth/google`,
{url: `/v1/auth/google`, method: 'POST', method: "POST",
headers: {'Content-Type': 'application/json', }, headers: { "Content-Type": "application/json" },
data: reqGoogleLogin, signal data: reqGoogleLogin,
signal,
}, },
options); options,
} );
};
export const getGoogleLoginMutationOptions = <
TError = void | HTTPValidationError,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof googleLogin>>,
TError,
{ data: ReqGoogleLogin },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof googleLogin>>,
TError,
{ data: ReqGoogleLogin },
TContext
> => {
const mutationKey = ["googleLogin"];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
export const getGoogleLoginMutationOptions = <TError = void | HTTPValidationError, const mutationFn: MutationFunction<
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof googleLogin>>, TError,{data: ReqGoogleLogin}, TContext>, request?: SecondParameter<typeof customFetch>} Awaited<ReturnType<typeof googleLogin>>,
): UseMutationOptions<Awaited<ReturnType<typeof googleLogin>>, TError,{data: ReqGoogleLogin}, TContext> => { { data: ReqGoogleLogin }
> = (props) => {
const { data } = props ?? {};
const mutationKey = ['googleLogin']; return googleLogin(data, requestOptions);
const {mutation: mutationOptions, request: requestOptions} = options ? };
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
return { mutationFn, ...mutationOptions };
};
export type GoogleLoginMutationResult = NonNullable<
Awaited<ReturnType<typeof googleLogin>>
>;
export type GoogleLoginMutationBody = ReqGoogleLogin;
export type GoogleLoginMutationError = void | HTTPValidationError;
const mutationFn: MutationFunction<Awaited<ReturnType<typeof googleLogin>>, {data: ReqGoogleLogin}> = (props) => { /**
const {data} = props ?? {};
return googleLogin(data,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type GoogleLoginMutationResult = NonNullable<Awaited<ReturnType<typeof googleLogin>>>
export type GoogleLoginMutationBody = ReqGoogleLogin
export type GoogleLoginMutationError = void | HTTPValidationError
/**
* @summary 구글 로그인 * @summary 구글 로그인
*/ */
export const useGoogleLogin = <TError = void | HTTPValidationError, export const useGoogleLogin = <
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof googleLogin>>, TError,{data: ReqGoogleLogin}, TContext>, request?: SecondParameter<typeof customFetch>} TError = void | HTTPValidationError,
, queryClient?: QueryClient): UseMutationResult< TContext = unknown,
Awaited<ReturnType<typeof googleLogin>>, >(
TError, options?: {
{data: ReqGoogleLogin}, mutation?: UseMutationOptions<
TContext Awaited<ReturnType<typeof googleLogin>>,
> => { TError,
{ data: ReqGoogleLogin },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof googleLogin>>,
TError,
{ data: ReqGoogleLogin },
TContext
> => {
const mutationOptions = getGoogleLoginMutationOptions(options);
const mutationOptions = getGoogleLoginMutationOptions(options); return useMutation(mutationOptions, queryClient);
};
return useMutation(mutationOptions, queryClient); /**
}
/**
* refresh 토큰으로 access 토큰을 재발급한다. * refresh 토큰으로 access 토큰을 재발급한다.
* @summary 액세스 토큰 갱신 * @summary 액세스 토큰 갱신
*/ */
export const refreshToken = ( export const refreshToken = (
options?: SecondParameter<typeof customFetch>,
options?: SecondParameter<typeof customFetch>,signal?: AbortSignal signal?: AbortSignal,
) => { ) => {
return customFetch<ResRefreshToken>(
{ url: `/v1/auth/refresh_token`, method: "POST", signal },
return customFetch<ResRefreshToken>( options,
{url: `/v1/auth/refresh_token`, method: 'POST', signal );
}, };
options);
}
export const getRefreshTokenMutationOptions = <
TError = void,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof refreshToken>>,
TError,
void,
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof refreshToken>>,
TError,
void,
TContext
> => {
const mutationKey = ["refreshToken"];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
export const getRefreshTokenMutationOptions = <TError = void, const mutationFn: MutationFunction<
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof refreshToken>>, TError,void, TContext>, request?: SecondParameter<typeof customFetch>} Awaited<ReturnType<typeof refreshToken>>,
): UseMutationOptions<Awaited<ReturnType<typeof refreshToken>>, TError,void, TContext> => { void
> = () => {
return refreshToken(requestOptions);
};
const mutationKey = ['refreshToken']; return { mutationFn, ...mutationOptions };
const {mutation: mutationOptions, request: requestOptions} = options ? };
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
export type RefreshTokenMutationResult = NonNullable<
Awaited<ReturnType<typeof refreshToken>>
>;
export type RefreshTokenMutationError = void;
const mutationFn: MutationFunction<Awaited<ReturnType<typeof refreshToken>>, void> = () => { /**
return refreshToken(requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type RefreshTokenMutationResult = NonNullable<Awaited<ReturnType<typeof refreshToken>>>
export type RefreshTokenMutationError = void
/**
* @summary 액세스 토큰 갱신 * @summary 액세스 토큰 갱신
*/ */
export const useRefreshToken = <TError = void, export const useRefreshToken = <TError = void, TContext = unknown>(
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof refreshToken>>, TError,void, TContext>, request?: SecondParameter<typeof customFetch>} options?: {
, queryClient?: QueryClient): UseMutationResult< mutation?: UseMutationOptions<
Awaited<ReturnType<typeof refreshToken>>, Awaited<ReturnType<typeof refreshToken>>,
TError, TError,
void, void,
TContext TContext
> => { >;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof refreshToken>>,
TError,
void,
TContext
> => {
const mutationOptions = getRefreshTokenMutationOptions(options);
const mutationOptions = getRefreshTokenMutationOptions(options); return useMutation(mutationOptions, queryClient);
};
return useMutation(mutationOptions, queryClient); /**
}
/**
* 유효한 access 토큰이 있어야 호출 가능. 토큰의 유저+회사 정보를 반환한다. * 유효한 access 토큰이 있어야 호출 가능. 토큰의 유저+회사 정보를 반환한다.
* @summary 내 정보 * @summary 내 정보
*/ */
export const me = ( export const me = (
options?: SecondParameter<typeof customFetch>,
options?: SecondParameter<typeof customFetch>,signal?: AbortSignal signal?: AbortSignal,
) => { ) => {
return customFetch<ResMe>(
{ url: `/v1/auth/me`, method: "GET", signal },
return customFetch<ResMe>( options,
{url: `/v1/auth/me`, method: 'GET', signal );
}, };
options);
}
export const getMeQueryKey = () => { export const getMeQueryKey = () => {
return [ return [`/v1/auth/me`] as const;
`/v1/auth/me` };
] as const;
}
export const getMeQueryOptions = <
export const getMeQueryOptions = <TData = Awaited<ReturnType<typeof me>>, TError = void>( options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof me>>, TError, TData>>, request?: SecondParameter<typeof customFetch>} TData = Awaited<ReturnType<typeof me>>,
) => { TError = void,
>(options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof me>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
}) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const {query: queryOptions, request: requestOptions} = options ?? {}; const queryKey = queryOptions?.queryKey ?? getMeQueryKey();
const queryKey = queryOptions?.queryKey ?? getMeQueryKey(); const queryFn: QueryFunction<Awaited<ReturnType<typeof me>>> = ({ signal }) =>
me(requestOptions, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof me>>,
const queryFn: QueryFunction<Awaited<ReturnType<typeof me>>> = ({ signal }) => me(requestOptions, signal); TError,
TData
> & { queryKey: DataTag<QueryKey, TData, TError> };
};
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof me>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
}
export type MeQueryResult = NonNullable<Awaited<ReturnType<typeof me>>>
export type MeQueryError = void
export type MeQueryResult = NonNullable<Awaited<ReturnType<typeof me>>>;
export type MeQueryError = void;
export function useMe<TData = Awaited<ReturnType<typeof me>>, TError = void>( export function useMe<TData = Awaited<ReturnType<typeof me>>, TError = void>(
options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof me>>, TError, TData>> & Pick< options: {
query: Partial<
UseQueryOptions<Awaited<ReturnType<typeof me>>, TError, TData>
> &
Pick<
DefinedInitialDataOptions< DefinedInitialDataOptions<
Awaited<ReturnType<typeof me>>, Awaited<ReturnType<typeof me>>,
TError, TError,
Awaited<ReturnType<typeof me>> Awaited<ReturnType<typeof me>>
> , 'initialData' >,
>, request?: SecondParameter<typeof customFetch>} "initialData"
, queryClient?: QueryClient >;
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useMe<TData = Awaited<ReturnType<typeof me>>, TError = void>( export function useMe<TData = Awaited<ReturnType<typeof me>>, TError = void>(
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof me>>, TError, TData>> & Pick< options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof me>>, TError, TData>
> &
Pick<
UndefinedInitialDataOptions< UndefinedInitialDataOptions<
Awaited<ReturnType<typeof me>>, Awaited<ReturnType<typeof me>>,
TError, TError,
Awaited<ReturnType<typeof me>> Awaited<ReturnType<typeof me>>
> , 'initialData' >,
>, request?: SecondParameter<typeof customFetch>} "initialData"
, queryClient?: QueryClient >;
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useMe<TData = Awaited<ReturnType<typeof me>>, TError = void>( export function useMe<TData = Awaited<ReturnType<typeof me>>, TError = void>(
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof me>>, TError, TData>>, request?: SecondParameter<typeof customFetch>} options?: {
, queryClient?: QueryClient query?: Partial<
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } UseQueryOptions<Awaited<ReturnType<typeof me>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/** /**
* @summary 내 정보 * @summary 내 정보
*/ */
export function useMe<TData = Awaited<ReturnType<typeof me>>, TError = void>( export function useMe<TData = Awaited<ReturnType<typeof me>>, TError = void>(
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof me>>, TError, TData>>, request?: SecondParameter<typeof customFetch>} options?: {
, queryClient?: QueryClient query?: Partial<
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } { UseQueryOptions<Awaited<ReturnType<typeof me>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions = getMeQueryOptions(options);
const queryOptions = getMeQueryOptions(options) const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }; query.queryKey = queryOptions.queryKey;
query.queryKey = queryOptions.queryKey ;
return query; return query;
} }
/** /**
* 본인 이름/이메일/연락처/비밀번호를 수정한다(권한·소속·ID 변경 불가). * 본인 이름/이메일/연락처/비밀번호를 수정한다(권한·소속·ID 변경 불가).
* @summary 내 정보 수정 * @summary 내 정보 수정
*/ */
export const updateMe = ( export const updateMe = (
reqUpdateMe: ReqUpdateMe, reqUpdateMe: ReqUpdateMe,
options?: SecondParameter<typeof customFetch>,) => { options?: SecondParameter<typeof customFetch>,
) => {
return customFetch<ResMe>(
return customFetch<ResMe>( {
{url: `/v1/auth/me`, method: 'PATCH', url: `/v1/auth/me`,
headers: {'Content-Type': 'application/json', }, method: "PATCH",
data: reqUpdateMe headers: { "Content-Type": "application/json" },
data: reqUpdateMe,
}, },
options); options,
} );
};
export const getUpdateMeMutationOptions = <
TError = void | HTTPValidationError,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateMe>>,
TError,
{ data: ReqUpdateMe },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateMe>>,
TError,
{ data: ReqUpdateMe },
TContext
> => {
const mutationKey = ["updateMe"];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
export const getUpdateMeMutationOptions = <TError = void | HTTPValidationError, const mutationFn: MutationFunction<
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateMe>>, TError,{data: ReqUpdateMe}, TContext>, request?: SecondParameter<typeof customFetch>} Awaited<ReturnType<typeof updateMe>>,
): UseMutationOptions<Awaited<ReturnType<typeof updateMe>>, TError,{data: ReqUpdateMe}, TContext> => { { data: ReqUpdateMe }
> = (props) => {
const { data } = props ?? {};
const mutationKey = ['updateMe']; return updateMe(data, requestOptions);
const {mutation: mutationOptions, request: requestOptions} = options ? };
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
return { mutationFn, ...mutationOptions };
};
export type UpdateMeMutationResult = NonNullable<
Awaited<ReturnType<typeof updateMe>>
>;
export type UpdateMeMutationBody = ReqUpdateMe;
export type UpdateMeMutationError = void | HTTPValidationError;
const mutationFn: MutationFunction<Awaited<ReturnType<typeof updateMe>>, {data: ReqUpdateMe}> = (props) => { /**
const {data} = props ?? {};
return updateMe(data,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type UpdateMeMutationResult = NonNullable<Awaited<ReturnType<typeof updateMe>>>
export type UpdateMeMutationBody = ReqUpdateMe
export type UpdateMeMutationError = void | HTTPValidationError
/**
* @summary 내 정보 수정 * @summary 내 정보 수정
*/ */
export const useUpdateMe = <TError = void | HTTPValidationError, export const useUpdateMe = <
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateMe>>, TError,{data: ReqUpdateMe}, TContext>, request?: SecondParameter<typeof customFetch>} TError = void | HTTPValidationError,
, queryClient?: QueryClient): UseMutationResult< TContext = unknown,
Awaited<ReturnType<typeof updateMe>>, >(
TError, options?: {
{data: ReqUpdateMe}, mutation?: UseMutationOptions<
TContext Awaited<ReturnType<typeof updateMe>>,
> => { TError,
{ data: ReqUpdateMe },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof updateMe>>,
TError,
{ data: ReqUpdateMe },
TContext
> => {
const mutationOptions = getUpdateMeMutationOptions(options);
const mutationOptions = getUpdateMeMutationOptions(options); return useMutation(mutationOptions, queryClient);
};
return useMutation(mutationOptions, queryClient);
}

View File

@ -4,9 +4,7 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import { import { useQuery } from "@tanstack/react-query";
useQuery
} from '@tanstack/react-query';
import type { import type {
DataTag, DataTag,
DefinedInitialDataOptions, DefinedInitialDataOptions,
@ -16,105 +14,163 @@ import type {
QueryKey, QueryKey,
UndefinedInitialDataOptions, UndefinedInitialDataOptions,
UseQueryOptions, UseQueryOptions,
UseQueryResult UseQueryResult,
} from '@tanstack/react-query'; } from "@tanstack/react-query";
import { customFetch } from '../../mutator/custom-fetch';
import { customFetch } from "../../mutator/custom-fetch";
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1]; type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/** /**
* @summary Healthz * @summary Healthz
*/ */
export const healthzHealthzGet = ( export const healthzHealthzGet = (
options?: SecondParameter<typeof customFetch>,
options?: SecondParameter<typeof customFetch>,signal?: AbortSignal signal?: AbortSignal,
) => { ) => {
return customFetch<unknown>(
{ url: `/healthz`, method: "GET", signal },
return customFetch<unknown>( options,
{url: `/healthz`, method: 'GET', signal );
}, };
options);
}
export const getHealthzHealthzGetQueryKey = () => { export const getHealthzHealthzGetQueryKey = () => {
return [ return [`/healthz`] as const;
`/healthz` };
] as const;
}
export const getHealthzHealthzGetQueryOptions = <
export const getHealthzHealthzGetQueryOptions = <TData = Awaited<ReturnType<typeof healthzHealthzGet>>, TError = void>( options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof healthzHealthzGet>>, TError, TData>>, request?: SecondParameter<typeof customFetch>} TData = Awaited<ReturnType<typeof healthzHealthzGet>>,
) => { TError = void,
>(options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof healthzHealthzGet>>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
}) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const {query: queryOptions, request: requestOptions} = options ?? {}; const queryKey = queryOptions?.queryKey ?? getHealthzHealthzGetQueryKey();
const queryKey = queryOptions?.queryKey ?? getHealthzHealthzGetQueryKey(); const queryFn: QueryFunction<
Awaited<ReturnType<typeof healthzHealthzGet>>
> = ({ signal }) => healthzHealthzGet(requestOptions, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof healthzHealthzGet>>,
TError,
TData
> & { queryKey: DataTag<QueryKey, TData, TError> };
};
const queryFn: QueryFunction<Awaited<ReturnType<typeof healthzHealthzGet>>> = ({ signal }) => healthzHealthzGet(requestOptions, signal); export type HealthzHealthzGetQueryResult = NonNullable<
Awaited<ReturnType<typeof healthzHealthzGet>>
>;
export type HealthzHealthzGetQueryError = void;
export function useHealthzHealthzGet<
TData = Awaited<ReturnType<typeof healthzHealthzGet>>,
TError = void,
>(
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof healthzHealthzGet>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> } options: {
} query: Partial<
UseQueryOptions<
export type HealthzHealthzGetQueryResult = NonNullable<Awaited<ReturnType<typeof healthzHealthzGet>>> Awaited<ReturnType<typeof healthzHealthzGet>>,
export type HealthzHealthzGetQueryError = void TError,
TData
>
export function useHealthzHealthzGet<TData = Awaited<ReturnType<typeof healthzHealthzGet>>, TError = void>( > &
options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof healthzHealthzGet>>, TError, TData>> & Pick< Pick<
DefinedInitialDataOptions< DefinedInitialDataOptions<
Awaited<ReturnType<typeof healthzHealthzGet>>, Awaited<ReturnType<typeof healthzHealthzGet>>,
TError, TError,
Awaited<ReturnType<typeof healthzHealthzGet>> Awaited<ReturnType<typeof healthzHealthzGet>>
> , 'initialData' >,
>, request?: SecondParameter<typeof customFetch>} "initialData"
, queryClient?: QueryClient >;
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } request?: SecondParameter<typeof customFetch>;
export function useHealthzHealthzGet<TData = Awaited<ReturnType<typeof healthzHealthzGet>>, TError = void>( },
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof healthzHealthzGet>>, TError, TData>> & Pick< queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useHealthzHealthzGet<
TData = Awaited<ReturnType<typeof healthzHealthzGet>>,
TError = void,
>(
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof healthzHealthzGet>>,
TError,
TData
>
> &
Pick<
UndefinedInitialDataOptions< UndefinedInitialDataOptions<
Awaited<ReturnType<typeof healthzHealthzGet>>, Awaited<ReturnType<typeof healthzHealthzGet>>,
TError, TError,
Awaited<ReturnType<typeof healthzHealthzGet>> Awaited<ReturnType<typeof healthzHealthzGet>>
> , 'initialData' >,
>, request?: SecondParameter<typeof customFetch>} "initialData"
, queryClient?: QueryClient >;
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } request?: SecondParameter<typeof customFetch>;
export function useHealthzHealthzGet<TData = Awaited<ReturnType<typeof healthzHealthzGet>>, TError = void>( },
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof healthzHealthzGet>>, TError, TData>>, request?: SecondParameter<typeof customFetch>} queryClient?: QueryClient,
, queryClient?: QueryClient ): UseQueryResult<TData, TError> & {
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } queryKey: DataTag<QueryKey, TData, TError>;
};
export function useHealthzHealthzGet<
TData = Awaited<ReturnType<typeof healthzHealthzGet>>,
TError = void,
>(
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof healthzHealthzGet>>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/** /**
* @summary Healthz * @summary Healthz
*/ */
export function useHealthzHealthzGet<TData = Awaited<ReturnType<typeof healthzHealthzGet>>, TError = void>( export function useHealthzHealthzGet<
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof healthzHealthzGet>>, TError, TData>>, request?: SecondParameter<typeof customFetch>} TData = Awaited<ReturnType<typeof healthzHealthzGet>>,
, queryClient?: QueryClient TError = void,
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } { >(
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof healthzHealthzGet>>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions = getHealthzHealthzGetQueryOptions(options);
const queryOptions = getHealthzHealthzGetQueryOptions(options) const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }; query.queryKey = queryOptions.queryKey;
query.queryKey = queryOptions.queryKey ;
return query; return query;
} }

View File

@ -4,10 +4,7 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import { import { useMutation, useQuery } from "@tanstack/react-query";
useMutation,
useQuery
} from '@tanstack/react-query';
import type { import type {
DataTag, DataTag,
DefinedInitialDataOptions, DefinedInitialDataOptions,
@ -20,8 +17,8 @@ import type {
UseMutationOptions, UseMutationOptions,
UseMutationResult, UseMutationResult,
UseQueryOptions, UseQueryOptions,
UseQueryResult UseQueryResult,
} from '@tanstack/react-query'; } from "@tanstack/react-query";
import type { import type {
HTTPValidationError, HTTPValidationError,
@ -32,407 +29,595 @@ import type {
ResCategorySchema, ResCategorySchema,
ResExtractFacts, ResExtractFacts,
ResFact, ResFact,
ResFactList ResFactList,
} from '.././model'; } from ".././model";
import { customFetch } from '../../mutator/custom-fetch';
import { customFetch } from "../../mutator/custom-fetch";
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1]; type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/** /**
* 이 사업장의 업종에 존재하는 fact 필드 정의. 관리 화면이 입력 폼을 그리는 데 쓴다. * 이 사업장의 업종에 존재하는 fact 필드 정의. 관리 화면이 입력 폼을 그리는 데 쓴다.
* @summary 업종 fact 스키마 * @summary 업종 fact 스키마
*/ */
export const getSchema = ( export const getSchema = (
placeId: string, placeId: string,
options?: SecondParameter<typeof customFetch>,signal?: AbortSignal options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => { ) => {
return customFetch<ResCategorySchema>(
{ url: `/v1/place/${placeId}/fact/schema`, method: "GET", signal },
return customFetch<ResCategorySchema>( options,
{url: `/v1/place/${placeId}/fact/schema`, method: 'GET', signal );
}, };
options);
}
export const getGetSchemaQueryKey = (placeId?: string) => {
return [`/v1/place/${placeId}/fact/schema`] as const;
};
export const getGetSchemaQueryOptions = <
export const getGetSchemaQueryKey = (placeId?: string,) => { TData = Awaited<ReturnType<typeof getSchema>>,
return [ TError = void | HTTPValidationError,
`/v1/place/${placeId}/fact/schema` >(
] as const; placeId: string,
} options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getSchema>>, TError, TData>
export const getGetSchemaQueryOptions = <TData = Awaited<ReturnType<typeof getSchema>>, TError = void | HTTPValidationError>(placeId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getSchema>>, TError, TData>>, request?: SecondParameter<typeof customFetch>} >;
request?: SecondParameter<typeof customFetch>;
},
) => { ) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const {query: queryOptions, request: requestOptions} = options ?? {}; const queryKey = queryOptions?.queryKey ?? getGetSchemaQueryKey(placeId);
const queryKey = queryOptions?.queryKey ?? getGetSchemaQueryKey(placeId); const queryFn: QueryFunction<Awaited<ReturnType<typeof getSchema>>> = ({
signal,
}) => getSchema(placeId, requestOptions, signal);
return {
queryKey,
queryFn,
enabled: !!placeId,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof getSchema>>, TError, TData> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
};
const queryFn: QueryFunction<Awaited<ReturnType<typeof getSchema>>> = ({ signal }) => getSchema(placeId, requestOptions, signal); export type GetSchemaQueryResult = NonNullable<
Awaited<ReturnType<typeof getSchema>>
>;
export type GetSchemaQueryError = void | HTTPValidationError;
export function useGetSchema<
TData = Awaited<ReturnType<typeof getSchema>>,
TError = void | HTTPValidationError,
>(
return { queryKey, queryFn, enabled: !!(placeId), ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getSchema>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> } placeId: string,
} options: {
query: Partial<
export type GetSchemaQueryResult = NonNullable<Awaited<ReturnType<typeof getSchema>>> UseQueryOptions<Awaited<ReturnType<typeof getSchema>>, TError, TData>
export type GetSchemaQueryError = void | HTTPValidationError > &
Pick<
export function useGetSchema<TData = Awaited<ReturnType<typeof getSchema>>, TError = void | HTTPValidationError>(
placeId: string, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getSchema>>, TError, TData>> & Pick<
DefinedInitialDataOptions< DefinedInitialDataOptions<
Awaited<ReturnType<typeof getSchema>>, Awaited<ReturnType<typeof getSchema>>,
TError, TError,
Awaited<ReturnType<typeof getSchema>> Awaited<ReturnType<typeof getSchema>>
> , 'initialData' >,
>, request?: SecondParameter<typeof customFetch>} "initialData"
, queryClient?: QueryClient >;
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } request?: SecondParameter<typeof customFetch>;
export function useGetSchema<TData = Awaited<ReturnType<typeof getSchema>>, TError = void | HTTPValidationError>( },
placeId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getSchema>>, TError, TData>> & Pick< queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useGetSchema<
TData = Awaited<ReturnType<typeof getSchema>>,
TError = void | HTTPValidationError,
>(
placeId: string,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getSchema>>, TError, TData>
> &
Pick<
UndefinedInitialDataOptions< UndefinedInitialDataOptions<
Awaited<ReturnType<typeof getSchema>>, Awaited<ReturnType<typeof getSchema>>,
TError, TError,
Awaited<ReturnType<typeof getSchema>> Awaited<ReturnType<typeof getSchema>>
> , 'initialData' >,
>, request?: SecondParameter<typeof customFetch>} "initialData"
, queryClient?: QueryClient >;
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } request?: SecondParameter<typeof customFetch>;
export function useGetSchema<TData = Awaited<ReturnType<typeof getSchema>>, TError = void | HTTPValidationError>( },
placeId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getSchema>>, TError, TData>>, request?: SecondParameter<typeof customFetch>} queryClient?: QueryClient,
, queryClient?: QueryClient ): UseQueryResult<TData, TError> & {
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } queryKey: DataTag<QueryKey, TData, TError>;
};
export function useGetSchema<
TData = Awaited<ReturnType<typeof getSchema>>,
TError = void | HTTPValidationError,
>(
placeId: string,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getSchema>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/** /**
* @summary 업종 fact 스키마 * @summary 업종 fact 스키마
*/ */
export function useGetSchema<TData = Awaited<ReturnType<typeof getSchema>>, TError = void | HTTPValidationError>( export function useGetSchema<
placeId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getSchema>>, TError, TData>>, request?: SecondParameter<typeof customFetch>} TData = Awaited<ReturnType<typeof getSchema>>,
, queryClient?: QueryClient TError = void | HTTPValidationError,
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } { >(
placeId: string,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getSchema>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions = getGetSchemaQueryOptions(placeId, options);
const queryOptions = getGetSchemaQueryOptions(placeId,options) const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }; query.queryKey = queryOptions.queryKey;
query.queryKey = queryOptions.queryKey ;
return query; return query;
} }
/** /**
* publishable_only=true 면 ★ 사이트에 나갈 수 있는 것(VERIFIED·CORRECTED)만. * publishable_only=true 면 ★ 사이트에 나갈 수 있는 것(VERIFIED·CORRECTED)만.
* @summary fact 목록 * @summary fact 목록
*/ */
export const listFacts = ( export const listFacts = (
placeId: string, placeId: string,
params?: ListFactsParams, params?: ListFactsParams,
options?: SecondParameter<typeof customFetch>,signal?: AbortSignal options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => { ) => {
return customFetch<ResFactList>(
{ url: `/v1/place/${placeId}/fact/list`, method: "GET", params, signal },
return customFetch<ResFactList>( options,
{url: `/v1/place/${placeId}/fact/list`, method: 'GET', );
params, signal };
},
options);
}
export const getListFactsQueryKey = (
placeId?: string,
export const getListFactsQueryKey = (placeId?: string, params?: ListFactsParams,
params?: ListFactsParams,) => {
return [
`/v1/place/${placeId}/fact/list`, ...(params ? [params]: [])
] as const;
}
export const getListFactsQueryOptions = <TData = Awaited<ReturnType<typeof listFacts>>, TError = void | HTTPValidationError>(placeId: string,
params?: ListFactsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listFacts>>, TError, TData>>, request?: SecondParameter<typeof customFetch>}
) => { ) => {
return [
`/v1/place/${placeId}/fact/list`,
...(params ? [params] : []),
] as const;
};
const {query: queryOptions, request: requestOptions} = options ?? {}; export const getListFactsQueryOptions = <
TData = Awaited<ReturnType<typeof listFacts>>,
TError = void | HTTPValidationError,
>(
placeId: string,
params?: ListFactsParams,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listFacts>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListFactsQueryKey(placeId,params); const queryKey =
queryOptions?.queryKey ?? getListFactsQueryKey(placeId, params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listFacts>>> = ({
signal,
}) => listFacts(placeId, params, requestOptions, signal);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listFacts>>> = ({ signal }) => listFacts(placeId,params, requestOptions, signal); return {
queryKey,
queryFn,
enabled: !!placeId,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof listFacts>>, TError, TData> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
};
export type ListFactsQueryResult = NonNullable<
Awaited<ReturnType<typeof listFacts>>
>;
export type ListFactsQueryError = void | HTTPValidationError;
export function useListFacts<
TData = Awaited<ReturnType<typeof listFacts>>,
return { queryKey, queryFn, enabled: !!(placeId), ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof listFacts>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> } TError = void | HTTPValidationError,
} >(
placeId: string,
export type ListFactsQueryResult = NonNullable<Awaited<ReturnType<typeof listFacts>>> params: undefined | ListFactsParams,
export type ListFactsQueryError = void | HTTPValidationError options: {
query: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listFacts>>, TError, TData>
export function useListFacts<TData = Awaited<ReturnType<typeof listFacts>>, TError = void | HTTPValidationError>( > &
placeId: string, Pick<
params: undefined | ListFactsParams, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof listFacts>>, TError, TData>> & Pick<
DefinedInitialDataOptions< DefinedInitialDataOptions<
Awaited<ReturnType<typeof listFacts>>, Awaited<ReturnType<typeof listFacts>>,
TError, TError,
Awaited<ReturnType<typeof listFacts>> Awaited<ReturnType<typeof listFacts>>
> , 'initialData' >,
>, request?: SecondParameter<typeof customFetch>} "initialData"
, queryClient?: QueryClient >;
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } request?: SecondParameter<typeof customFetch>;
export function useListFacts<TData = Awaited<ReturnType<typeof listFacts>>, TError = void | HTTPValidationError>( },
placeId: string, queryClient?: QueryClient,
params?: ListFactsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listFacts>>, TError, TData>> & Pick< ): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useListFacts<
TData = Awaited<ReturnType<typeof listFacts>>,
TError = void | HTTPValidationError,
>(
placeId: string,
params?: ListFactsParams,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listFacts>>, TError, TData>
> &
Pick<
UndefinedInitialDataOptions< UndefinedInitialDataOptions<
Awaited<ReturnType<typeof listFacts>>, Awaited<ReturnType<typeof listFacts>>,
TError, TError,
Awaited<ReturnType<typeof listFacts>> Awaited<ReturnType<typeof listFacts>>
> , 'initialData' >,
>, request?: SecondParameter<typeof customFetch>} "initialData"
, queryClient?: QueryClient >;
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } request?: SecondParameter<typeof customFetch>;
export function useListFacts<TData = Awaited<ReturnType<typeof listFacts>>, TError = void | HTTPValidationError>( },
placeId: string, queryClient?: QueryClient,
params?: ListFactsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listFacts>>, TError, TData>>, request?: SecondParameter<typeof customFetch>} ): UseQueryResult<TData, TError> & {
, queryClient?: QueryClient queryKey: DataTag<QueryKey, TData, TError>;
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } };
export function useListFacts<
TData = Awaited<ReturnType<typeof listFacts>>,
TError = void | HTTPValidationError,
>(
placeId: string,
params?: ListFactsParams,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listFacts>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/** /**
* @summary fact 목록 * @summary fact 목록
*/ */
export function useListFacts<TData = Awaited<ReturnType<typeof listFacts>>, TError = void | HTTPValidationError>( export function useListFacts<
placeId: string, TData = Awaited<ReturnType<typeof listFacts>>,
params?: ListFactsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listFacts>>, TError, TData>>, request?: SecondParameter<typeof customFetch>} TError = void | HTTPValidationError,
, queryClient?: QueryClient >(
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } { placeId: string,
params?: ListFactsParams,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listFacts>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions = getListFactsQueryOptions(placeId, params, options);
const queryOptions = getListFactsQueryOptions(placeId,params,options) const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }; query.queryKey = queryOptions.queryKey;
query.queryKey = queryOptions.queryKey ;
return query; return query;
} }
/** /**
* 같은 key 의 활성 fact 는 EXPIRED 로 밀리고 새 행이 들어간다(값 이력 보존). ★ 자동 수집(api/crawl/llm)은 사장님 수정본(CORRECTED)을 덮어쓸 수 없다. * 같은 key 의 활성 fact 는 EXPIRED 로 밀리고 새 행이 들어간다(값 이력 보존). ★ 자동 수집(api/crawl/llm)은 사장님 수정본(CORRECTED)을 덮어쓸 수 없다.
* @summary fact 기록 * @summary fact 기록
*/ */
export const upsertFact = ( export const upsertFact = (
placeId: string, placeId: string,
reqUpsertFact: ReqUpsertFact, reqUpsertFact: ReqUpsertFact,
options?: SecondParameter<typeof customFetch>,signal?: AbortSignal options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => { ) => {
return customFetch<ResFact>(
{
return customFetch<ResFact>( url: `/v1/place/${placeId}/fact`,
{url: `/v1/place/${placeId}/fact`, method: 'POST', method: "POST",
headers: {'Content-Type': 'application/json', }, headers: { "Content-Type": "application/json" },
data: reqUpsertFact, signal data: reqUpsertFact,
signal,
}, },
options); options,
} );
};
export const getUpsertFactMutationOptions = <
TError = void | HTTPValidationError,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof upsertFact>>,
TError,
{ placeId: string; data: ReqUpsertFact },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof upsertFact>>,
TError,
{ placeId: string; data: ReqUpsertFact },
TContext
> => {
const mutationKey = ["upsertFact"];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
export const getUpsertFactMutationOptions = <TError = void | HTTPValidationError, const mutationFn: MutationFunction<
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof upsertFact>>, TError,{placeId: string;data: ReqUpsertFact}, TContext>, request?: SecondParameter<typeof customFetch>} Awaited<ReturnType<typeof upsertFact>>,
): UseMutationOptions<Awaited<ReturnType<typeof upsertFact>>, TError,{placeId: string;data: ReqUpsertFact}, TContext> => { { placeId: string; data: ReqUpsertFact }
> = (props) => {
const { placeId, data } = props ?? {};
const mutationKey = ['upsertFact']; return upsertFact(placeId, data, requestOptions);
const {mutation: mutationOptions, request: requestOptions} = options ? };
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
return { mutationFn, ...mutationOptions };
};
export type UpsertFactMutationResult = NonNullable<
Awaited<ReturnType<typeof upsertFact>>
>;
export type UpsertFactMutationBody = ReqUpsertFact;
export type UpsertFactMutationError = void | HTTPValidationError;
const mutationFn: MutationFunction<Awaited<ReturnType<typeof upsertFact>>, {placeId: string;data: ReqUpsertFact}> = (props) => { /**
const {placeId,data} = props ?? {};
return upsertFact(placeId,data,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type UpsertFactMutationResult = NonNullable<Awaited<ReturnType<typeof upsertFact>>>
export type UpsertFactMutationBody = ReqUpsertFact
export type UpsertFactMutationError = void | HTTPValidationError
/**
* @summary fact 기록 * @summary fact 기록
*/ */
export const useUpsertFact = <TError = void | HTTPValidationError, export const useUpsertFact = <
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof upsertFact>>, TError,{placeId: string;data: ReqUpsertFact}, TContext>, request?: SecondParameter<typeof customFetch>} TError = void | HTTPValidationError,
, queryClient?: QueryClient): UseMutationResult< TContext = unknown,
Awaited<ReturnType<typeof upsertFact>>, >(
TError, options?: {
{placeId: string;data: ReqUpsertFact}, mutation?: UseMutationOptions<
TContext Awaited<ReturnType<typeof upsertFact>>,
> => { TError,
{ placeId: string; data: ReqUpsertFact },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof upsertFact>>,
TError,
{ placeId: string; data: ReqUpsertFact },
TContext
> => {
const mutationOptions = getUpsertFactMutationOptions(options);
const mutationOptions = getUpsertFactMutationOptions(options); return useMutation(mutationOptions, queryClient);
};
return useMutation(mutationOptions, queryClient); /**
}
/**
* 허용 전이만 통과한다(FACT_STATUS_TRANSITIONS). CORRECTED 로 갈 때는 value 가 필수다. * 허용 전이만 통과한다(FACT_STATUS_TRANSITIONS). CORRECTED 로 갈 때는 value 가 필수다.
* @summary 검증 상태 전이 * @summary 검증 상태 전이
*/ */
export const transitionFact = ( export const transitionFact = (
placeId: string, placeId: string,
factId: string, factId: string,
reqTransitionFact: ReqTransitionFact, reqTransitionFact: ReqTransitionFact,
options?: SecondParameter<typeof customFetch>,signal?: AbortSignal options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => { ) => {
return customFetch<ResFact>(
{
return customFetch<ResFact>( url: `/v1/place/${placeId}/fact/${factId}/transition`,
{url: `/v1/place/${placeId}/fact/${factId}/transition`, method: 'POST', method: "POST",
headers: {'Content-Type': 'application/json', }, headers: { "Content-Type": "application/json" },
data: reqTransitionFact, signal data: reqTransitionFact,
signal,
}, },
options); options,
} );
};
export const getTransitionFactMutationOptions = <
TError = void | HTTPValidationError,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof transitionFact>>,
TError,
{ placeId: string; factId: string; data: ReqTransitionFact },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof transitionFact>>,
TError,
{ placeId: string; factId: string; data: ReqTransitionFact },
TContext
> => {
const mutationKey = ["transitionFact"];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
export const getTransitionFactMutationOptions = <TError = void | HTTPValidationError, const mutationFn: MutationFunction<
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof transitionFact>>, TError,{placeId: string;factId: string;data: ReqTransitionFact}, TContext>, request?: SecondParameter<typeof customFetch>} Awaited<ReturnType<typeof transitionFact>>,
): UseMutationOptions<Awaited<ReturnType<typeof transitionFact>>, TError,{placeId: string;factId: string;data: ReqTransitionFact}, TContext> => { { placeId: string; factId: string; data: ReqTransitionFact }
> = (props) => {
const { placeId, factId, data } = props ?? {};
const mutationKey = ['transitionFact']; return transitionFact(placeId, factId, data, requestOptions);
const {mutation: mutationOptions, request: requestOptions} = options ? };
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
return { mutationFn, ...mutationOptions };
};
export type TransitionFactMutationResult = NonNullable<
Awaited<ReturnType<typeof transitionFact>>
>;
export type TransitionFactMutationBody = ReqTransitionFact;
export type TransitionFactMutationError = void | HTTPValidationError;
const mutationFn: MutationFunction<Awaited<ReturnType<typeof transitionFact>>, {placeId: string;factId: string;data: ReqTransitionFact}> = (props) => { /**
const {placeId,factId,data} = props ?? {};
return transitionFact(placeId,factId,data,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type TransitionFactMutationResult = NonNullable<Awaited<ReturnType<typeof transitionFact>>>
export type TransitionFactMutationBody = ReqTransitionFact
export type TransitionFactMutationError = void | HTTPValidationError
/**
* @summary 검증 상태 전이 * @summary 검증 상태 전이
*/ */
export const useTransitionFact = <TError = void | HTTPValidationError, export const useTransitionFact = <
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof transitionFact>>, TError,{placeId: string;factId: string;data: ReqTransitionFact}, TContext>, request?: SecondParameter<typeof customFetch>} TError = void | HTTPValidationError,
, queryClient?: QueryClient): UseMutationResult< TContext = unknown,
Awaited<ReturnType<typeof transitionFact>>, >(
TError, options?: {
{placeId: string;factId: string;data: ReqTransitionFact}, mutation?: UseMutationOptions<
TContext Awaited<ReturnType<typeof transitionFact>>,
> => { TError,
{ placeId: string; factId: string; data: ReqTransitionFact },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof transitionFact>>,
TError,
{ placeId: string; factId: string; data: ReqTransitionFact },
TContext
> => {
const mutationOptions = getTransitionFactMutationOptions(options);
const mutationOptions = getTransitionFactMutationOptions(options); return useMutation(mutationOptions, queryClient);
};
return useMutation(mutationOptions, queryClient); /**
}
/**
* 사장님이 붙여넣은 소개글·안내문에서 업종 스키마 fact 를 뽑는다. ★ 뽑은 값의 **근거 문장이 원문에 실제로 있는지 대조**해 통과한 것만 저장한다. 저장된 값은 전부 후보(UNVERIFIED)라 사장님이 확인해야 사이트에 나간다. 버려진 항목은 사유와 함께 돌려준다. * 사장님이 붙여넣은 소개글·안내문에서 업종 스키마 fact 를 뽑는다. ★ 뽑은 값의 **근거 문장이 원문에 실제로 있는지 대조**해 통과한 것만 저장한다. 저장된 값은 전부 후보(UNVERIFIED)라 사장님이 확인해야 사이트에 나간다. 버려진 항목은 사유와 함께 돌려준다.
* @summary 붙여넣은 텍스트에서 fact 추출 * @summary 붙여넣은 텍스트에서 fact 추출
*/ */
export const extractFactsFromText = ( export const extractFactsFromText = (
placeId: string, placeId: string,
reqExtractFacts: ReqExtractFacts, reqExtractFacts: ReqExtractFacts,
options?: SecondParameter<typeof customFetch>,signal?: AbortSignal options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => { ) => {
return customFetch<ResExtractFacts>(
{
return customFetch<ResExtractFacts>( url: `/v1/place/${placeId}/fact/extract`,
{url: `/v1/place/${placeId}/fact/extract`, method: 'POST', method: "POST",
headers: {'Content-Type': 'application/json', }, headers: { "Content-Type": "application/json" },
data: reqExtractFacts, signal data: reqExtractFacts,
signal,
}, },
options); options,
} );
};
export const getExtractFactsFromTextMutationOptions = <
TError = void | HTTPValidationError,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof extractFactsFromText>>,
TError,
{ placeId: string; data: ReqExtractFacts },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof extractFactsFromText>>,
TError,
{ placeId: string; data: ReqExtractFacts },
TContext
> => {
const mutationKey = ["extractFactsFromText"];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
export const getExtractFactsFromTextMutationOptions = <TError = void | HTTPValidationError, const mutationFn: MutationFunction<
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof extractFactsFromText>>, TError,{placeId: string;data: ReqExtractFacts}, TContext>, request?: SecondParameter<typeof customFetch>} Awaited<ReturnType<typeof extractFactsFromText>>,
): UseMutationOptions<Awaited<ReturnType<typeof extractFactsFromText>>, TError,{placeId: string;data: ReqExtractFacts}, TContext> => { { placeId: string; data: ReqExtractFacts }
> = (props) => {
const { placeId, data } = props ?? {};
const mutationKey = ['extractFactsFromText']; return extractFactsFromText(placeId, data, requestOptions);
const {mutation: mutationOptions, request: requestOptions} = options ? };
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
return { mutationFn, ...mutationOptions };
};
export type ExtractFactsFromTextMutationResult = NonNullable<
Awaited<ReturnType<typeof extractFactsFromText>>
>;
export type ExtractFactsFromTextMutationBody = ReqExtractFacts;
export type ExtractFactsFromTextMutationError = void | HTTPValidationError;
const mutationFn: MutationFunction<Awaited<ReturnType<typeof extractFactsFromText>>, {placeId: string;data: ReqExtractFacts}> = (props) => { /**
const {placeId,data} = props ?? {};
return extractFactsFromText(placeId,data,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type ExtractFactsFromTextMutationResult = NonNullable<Awaited<ReturnType<typeof extractFactsFromText>>>
export type ExtractFactsFromTextMutationBody = ReqExtractFacts
export type ExtractFactsFromTextMutationError = void | HTTPValidationError
/**
* @summary 붙여넣은 텍스트에서 fact 추출 * @summary 붙여넣은 텍스트에서 fact 추출
*/ */
export const useExtractFactsFromText = <TError = void | HTTPValidationError, export const useExtractFactsFromText = <
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof extractFactsFromText>>, TError,{placeId: string;data: ReqExtractFacts}, TContext>, request?: SecondParameter<typeof customFetch>} TError = void | HTTPValidationError,
, queryClient?: QueryClient): UseMutationResult< TContext = unknown,
Awaited<ReturnType<typeof extractFactsFromText>>, >(
TError, options?: {
{placeId: string;data: ReqExtractFacts}, mutation?: UseMutationOptions<
TContext Awaited<ReturnType<typeof extractFactsFromText>>,
> => { TError,
{ placeId: string; data: ReqExtractFacts },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof extractFactsFromText>>,
TError,
{ placeId: string; data: ReqExtractFacts },
TContext
> => {
const mutationOptions = getExtractFactsFromTextMutationOptions(options);
const mutationOptions = getExtractFactsFromTextMutationOptions(options); return useMutation(mutationOptions, queryClient);
};
return useMutation(mutationOptions, queryClient);
}

View File

@ -4,10 +4,7 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import { import { useMutation, useQuery } from "@tanstack/react-query";
useMutation,
useQuery
} from '@tanstack/react-query';
import type { import type {
DataTag, DataTag,
DefinedInitialDataOptions, DefinedInitialDataOptions,
@ -20,8 +17,8 @@ import type {
UseMutationOptions, UseMutationOptions,
UseMutationResult, UseMutationResult,
UseQueryOptions, UseQueryOptions,
UseQueryResult UseQueryResult,
} from '@tanstack/react-query'; } from "@tanstack/react-query";
import type { import type {
HTTPValidationError, HTTPValidationError,
@ -29,248 +26,356 @@ import type {
ReqCreateFaq, ReqCreateFaq,
ReqTransitionFaq, ReqTransitionFaq,
ResFaq, ResFaq,
ResFaqList ResFaqList,
} from '.././model'; } from ".././model";
import { customFetch } from '../../mutator/custom-fetch';
import { customFetch } from "../../mutator/custom-fetch";
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1]; type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/** /**
* publishable_only=true 면 ★ 사이트에 나갈 수 있는 것(VERIFIED·CORRECTED)만. 기본은 승인 대기(UNVERIFIED)까지 함께 준다 — 승인할 대상을 봐야 하기 때문이다. * publishable_only=true 면 ★ 사이트에 나갈 수 있는 것(VERIFIED·CORRECTED)만. 기본은 승인 대기(UNVERIFIED)까지 함께 준다 — 승인할 대상을 봐야 하기 때문이다.
* @summary FAQ 목록 * @summary FAQ 목록
*/ */
export const listFaqs = ( export const listFaqs = (
placeId: string, placeId: string,
params?: ListFaqsParams, params?: ListFaqsParams,
options?: SecondParameter<typeof customFetch>,signal?: AbortSignal options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => { ) => {
return customFetch<ResFaqList>(
{ url: `/v1/place/${placeId}/faq/list`, method: "GET", params, signal },
return customFetch<ResFaqList>( options,
{url: `/v1/place/${placeId}/faq/list`, method: 'GET', );
params, signal };
},
options);
}
export const getListFaqsQueryKey = (
placeId?: string,
export const getListFaqsQueryKey = (placeId?: string, params?: ListFaqsParams,
params?: ListFaqsParams,) => {
return [
`/v1/place/${placeId}/faq/list`, ...(params ? [params]: [])
] as const;
}
export const getListFaqsQueryOptions = <TData = Awaited<ReturnType<typeof listFaqs>>, TError = void | HTTPValidationError>(placeId: string,
params?: ListFaqsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listFaqs>>, TError, TData>>, request?: SecondParameter<typeof customFetch>}
) => { ) => {
return [
`/v1/place/${placeId}/faq/list`,
...(params ? [params] : []),
] as const;
};
const {query: queryOptions, request: requestOptions} = options ?? {}; export const getListFaqsQueryOptions = <
TData = Awaited<ReturnType<typeof listFaqs>>,
TError = void | HTTPValidationError,
>(
placeId: string,
params?: ListFaqsParams,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listFaqs>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListFaqsQueryKey(placeId,params); const queryKey =
queryOptions?.queryKey ?? getListFaqsQueryKey(placeId, params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listFaqs>>> = ({
signal,
}) => listFaqs(placeId, params, requestOptions, signal);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listFaqs>>> = ({ signal }) => listFaqs(placeId,params, requestOptions, signal); return {
queryKey,
queryFn,
enabled: !!placeId,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof listFaqs>>, TError, TData> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
};
export type ListFaqsQueryResult = NonNullable<
Awaited<ReturnType<typeof listFaqs>>
>;
export type ListFaqsQueryError = void | HTTPValidationError;
export function useListFaqs<
TData = Awaited<ReturnType<typeof listFaqs>>,
return { queryKey, queryFn, enabled: !!(placeId), ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof listFaqs>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> } TError = void | HTTPValidationError,
} >(
placeId: string,
export type ListFaqsQueryResult = NonNullable<Awaited<ReturnType<typeof listFaqs>>> params: undefined | ListFaqsParams,
export type ListFaqsQueryError = void | HTTPValidationError options: {
query: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listFaqs>>, TError, TData>
export function useListFaqs<TData = Awaited<ReturnType<typeof listFaqs>>, TError = void | HTTPValidationError>( > &
placeId: string, Pick<
params: undefined | ListFaqsParams, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof listFaqs>>, TError, TData>> & Pick<
DefinedInitialDataOptions< DefinedInitialDataOptions<
Awaited<ReturnType<typeof listFaqs>>, Awaited<ReturnType<typeof listFaqs>>,
TError, TError,
Awaited<ReturnType<typeof listFaqs>> Awaited<ReturnType<typeof listFaqs>>
> , 'initialData' >,
>, request?: SecondParameter<typeof customFetch>} "initialData"
, queryClient?: QueryClient >;
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } request?: SecondParameter<typeof customFetch>;
export function useListFaqs<TData = Awaited<ReturnType<typeof listFaqs>>, TError = void | HTTPValidationError>( },
placeId: string, queryClient?: QueryClient,
params?: ListFaqsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listFaqs>>, TError, TData>> & Pick< ): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useListFaqs<
TData = Awaited<ReturnType<typeof listFaqs>>,
TError = void | HTTPValidationError,
>(
placeId: string,
params?: ListFaqsParams,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listFaqs>>, TError, TData>
> &
Pick<
UndefinedInitialDataOptions< UndefinedInitialDataOptions<
Awaited<ReturnType<typeof listFaqs>>, Awaited<ReturnType<typeof listFaqs>>,
TError, TError,
Awaited<ReturnType<typeof listFaqs>> Awaited<ReturnType<typeof listFaqs>>
> , 'initialData' >,
>, request?: SecondParameter<typeof customFetch>} "initialData"
, queryClient?: QueryClient >;
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } request?: SecondParameter<typeof customFetch>;
export function useListFaqs<TData = Awaited<ReturnType<typeof listFaqs>>, TError = void | HTTPValidationError>( },
placeId: string, queryClient?: QueryClient,
params?: ListFaqsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listFaqs>>, TError, TData>>, request?: SecondParameter<typeof customFetch>} ): UseQueryResult<TData, TError> & {
, queryClient?: QueryClient queryKey: DataTag<QueryKey, TData, TError>;
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } };
export function useListFaqs<
TData = Awaited<ReturnType<typeof listFaqs>>,
TError = void | HTTPValidationError,
>(
placeId: string,
params?: ListFaqsParams,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listFaqs>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/** /**
* @summary FAQ 목록 * @summary FAQ 목록
*/ */
export function useListFaqs<TData = Awaited<ReturnType<typeof listFaqs>>, TError = void | HTTPValidationError>( export function useListFaqs<
placeId: string, TData = Awaited<ReturnType<typeof listFaqs>>,
params?: ListFaqsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listFaqs>>, TError, TData>>, request?: SecondParameter<typeof customFetch>} TError = void | HTTPValidationError,
, queryClient?: QueryClient >(
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } { placeId: string,
params?: ListFaqsParams,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listFaqs>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions = getListFaqsQueryOptions(placeId, params, options);
const queryOptions = getListFaqsQueryOptions(placeId,params,options) const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }; query.queryKey = queryOptions.queryKey;
query.queryKey = queryOptions.queryKey ;
return query; return query;
} }
/** /**
* 사장님이 직접 쓴 FAQ. fact 의 owner 입력과 같이 바로 노출값(VERIFIED)이 된다 — 사람이 곧 출처다. 출처(generated_by)는 서버가 OWNER 로 고정한다. * 사장님이 직접 쓴 FAQ. fact 의 owner 입력과 같이 바로 노출값(VERIFIED)이 된다 — 사람이 곧 출처다. 출처(generated_by)는 서버가 OWNER 로 고정한다.
* @summary FAQ 직접 추가 * @summary FAQ 직접 추가
*/ */
export const createFaq = ( export const createFaq = (
placeId: string, placeId: string,
reqCreateFaq: ReqCreateFaq, reqCreateFaq: ReqCreateFaq,
options?: SecondParameter<typeof customFetch>,signal?: AbortSignal options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => { ) => {
return customFetch<ResFaq>(
{
return customFetch<ResFaq>( url: `/v1/place/${placeId}/faq`,
{url: `/v1/place/${placeId}/faq`, method: 'POST', method: "POST",
headers: {'Content-Type': 'application/json', }, headers: { "Content-Type": "application/json" },
data: reqCreateFaq, signal data: reqCreateFaq,
signal,
}, },
options); options,
} );
};
export const getCreateFaqMutationOptions = <
TError = void | HTTPValidationError,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createFaq>>,
TError,
{ placeId: string; data: ReqCreateFaq },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createFaq>>,
TError,
{ placeId: string; data: ReqCreateFaq },
TContext
> => {
const mutationKey = ["createFaq"];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
export const getCreateFaqMutationOptions = <TError = void | HTTPValidationError, const mutationFn: MutationFunction<
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createFaq>>, TError,{placeId: string;data: ReqCreateFaq}, TContext>, request?: SecondParameter<typeof customFetch>} Awaited<ReturnType<typeof createFaq>>,
): UseMutationOptions<Awaited<ReturnType<typeof createFaq>>, TError,{placeId: string;data: ReqCreateFaq}, TContext> => { { placeId: string; data: ReqCreateFaq }
> = (props) => {
const { placeId, data } = props ?? {};
const mutationKey = ['createFaq']; return createFaq(placeId, data, requestOptions);
const {mutation: mutationOptions, request: requestOptions} = options ? };
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
return { mutationFn, ...mutationOptions };
};
export type CreateFaqMutationResult = NonNullable<
Awaited<ReturnType<typeof createFaq>>
>;
export type CreateFaqMutationBody = ReqCreateFaq;
export type CreateFaqMutationError = void | HTTPValidationError;
const mutationFn: MutationFunction<Awaited<ReturnType<typeof createFaq>>, {placeId: string;data: ReqCreateFaq}> = (props) => { /**
const {placeId,data} = props ?? {};
return createFaq(placeId,data,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type CreateFaqMutationResult = NonNullable<Awaited<ReturnType<typeof createFaq>>>
export type CreateFaqMutationBody = ReqCreateFaq
export type CreateFaqMutationError = void | HTTPValidationError
/**
* @summary FAQ 직접 추가 * @summary FAQ 직접 추가
*/ */
export const useCreateFaq = <TError = void | HTTPValidationError, export const useCreateFaq = <
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createFaq>>, TError,{placeId: string;data: ReqCreateFaq}, TContext>, request?: SecondParameter<typeof customFetch>} TError = void | HTTPValidationError,
, queryClient?: QueryClient): UseMutationResult< TContext = unknown,
Awaited<ReturnType<typeof createFaq>>, >(
TError, options?: {
{placeId: string;data: ReqCreateFaq}, mutation?: UseMutationOptions<
TContext Awaited<ReturnType<typeof createFaq>>,
> => { TError,
{ placeId: string; data: ReqCreateFaq },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof createFaq>>,
TError,
{ placeId: string; data: ReqCreateFaq },
TContext
> => {
const mutationOptions = getCreateFaqMutationOptions(options);
const mutationOptions = getCreateFaqMutationOptions(options); return useMutation(mutationOptions, queryClient);
};
return useMutation(mutationOptions, queryClient); /**
}
/**
* 허용 전이만 통과한다(FACT_STATUS_TRANSITIONS — fact 와 같은 표). CORRECTED 로 갈 때는 고친 question/answer 중 하나 이상이 필요하고, 그 뒤로는 ★ 잠긴 상태라 재생성이 덮어쓰지 못한다. * 허용 전이만 통과한다(FACT_STATUS_TRANSITIONS — fact 와 같은 표). CORRECTED 로 갈 때는 고친 question/answer 중 하나 이상이 필요하고, 그 뒤로는 ★ 잠긴 상태라 재생성이 덮어쓰지 못한다.
* @summary 검증 상태 전이(승인·정정·반려) * @summary 검증 상태 전이(승인·정정·반려)
*/ */
export const transitionFaq = ( export const transitionFaq = (
placeId: string, placeId: string,
faqId: string, faqId: string,
reqTransitionFaq: ReqTransitionFaq, reqTransitionFaq: ReqTransitionFaq,
options?: SecondParameter<typeof customFetch>,signal?: AbortSignal options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => { ) => {
return customFetch<ResFaq>(
{
return customFetch<ResFaq>( url: `/v1/place/${placeId}/faq/${faqId}/transition`,
{url: `/v1/place/${placeId}/faq/${faqId}/transition`, method: 'POST', method: "POST",
headers: {'Content-Type': 'application/json', }, headers: { "Content-Type": "application/json" },
data: reqTransitionFaq, signal data: reqTransitionFaq,
signal,
}, },
options); options,
} );
};
export const getTransitionFaqMutationOptions = <
TError = void | HTTPValidationError,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof transitionFaq>>,
TError,
{ placeId: string; faqId: string; data: ReqTransitionFaq },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof transitionFaq>>,
TError,
{ placeId: string; faqId: string; data: ReqTransitionFaq },
TContext
> => {
const mutationKey = ["transitionFaq"];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
export const getTransitionFaqMutationOptions = <TError = void | HTTPValidationError, const mutationFn: MutationFunction<
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof transitionFaq>>, TError,{placeId: string;faqId: string;data: ReqTransitionFaq}, TContext>, request?: SecondParameter<typeof customFetch>} Awaited<ReturnType<typeof transitionFaq>>,
): UseMutationOptions<Awaited<ReturnType<typeof transitionFaq>>, TError,{placeId: string;faqId: string;data: ReqTransitionFaq}, TContext> => { { placeId: string; faqId: string; data: ReqTransitionFaq }
> = (props) => {
const { placeId, faqId, data } = props ?? {};
const mutationKey = ['transitionFaq']; return transitionFaq(placeId, faqId, data, requestOptions);
const {mutation: mutationOptions, request: requestOptions} = options ? };
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
return { mutationFn, ...mutationOptions };
};
export type TransitionFaqMutationResult = NonNullable<
Awaited<ReturnType<typeof transitionFaq>>
>;
export type TransitionFaqMutationBody = ReqTransitionFaq;
export type TransitionFaqMutationError = void | HTTPValidationError;
const mutationFn: MutationFunction<Awaited<ReturnType<typeof transitionFaq>>, {placeId: string;faqId: string;data: ReqTransitionFaq}> = (props) => { /**
const {placeId,faqId,data} = props ?? {};
return transitionFaq(placeId,faqId,data,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type TransitionFaqMutationResult = NonNullable<Awaited<ReturnType<typeof transitionFaq>>>
export type TransitionFaqMutationBody = ReqTransitionFaq
export type TransitionFaqMutationError = void | HTTPValidationError
/**
* @summary 검증 상태 전이(승인·정정·반려) * @summary 검증 상태 전이(승인·정정·반려)
*/ */
export const useTransitionFaq = <TError = void | HTTPValidationError, export const useTransitionFaq = <
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof transitionFaq>>, TError,{placeId: string;faqId: string;data: ReqTransitionFaq}, TContext>, request?: SecondParameter<typeof customFetch>} TError = void | HTTPValidationError,
, queryClient?: QueryClient): UseMutationResult< TContext = unknown,
Awaited<ReturnType<typeof transitionFaq>>, >(
TError, options?: {
{placeId: string;faqId: string;data: ReqTransitionFaq}, mutation?: UseMutationOptions<
TContext Awaited<ReturnType<typeof transitionFaq>>,
> => { TError,
{ placeId: string; faqId: string; data: ReqTransitionFaq },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof transitionFaq>>,
TError,
{ placeId: string; faqId: string; data: ReqTransitionFaq },
TContext
> => {
const mutationOptions = getTransitionFaqMutationOptions(options);
const mutationOptions = getTransitionFaqMutationOptions(options); return useMutation(mutationOptions, queryClient);
};
return useMutation(mutationOptions, queryClient);
}

View File

@ -0,0 +1,166 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Web4Ai API
* OpenAPI spec version: 0.1.0
*/
import { useQuery } from "@tanstack/react-query";
import type {
DataTag,
DefinedInitialDataOptions,
DefinedUseQueryResult,
QueryClient,
QueryFunction,
QueryKey,
UndefinedInitialDataOptions,
UseQueryOptions,
UseQueryResult,
} from "@tanstack/react-query";
import type { HTTPValidationError, RelayParams } from ".././model";
import { customFetch } from "../../mutator/custom-fetch";
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/**
* 인증을 요구하지 않는다. 발행본은 로그인 없이 열리는 정적 페이지이고, 여기서 나가는
것은 **그 페이지가 이미 화면에 싣고 있는 사진**뿐이다(allowlist 가 그걸 보장한다).
* @summary 사진 중계 — 캔버스 오염을 피하려고 같은 오리진으로 흘려보낸다
*/
export const relay = (
params: RelayParams,
options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => {
return customFetch<unknown>(
{ url: `/v1/image/relay`, method: "GET", params, signal },
options,
);
};
export const getRelayQueryKey = (params?: RelayParams) => {
return [`/v1/image/relay`, ...(params ? [params] : [])] as const;
};
export const getRelayQueryOptions = <
TData = Awaited<ReturnType<typeof relay>>,
TError = HTTPValidationError,
>(
params: RelayParams,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof relay>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getRelayQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof relay>>> = ({
signal,
}) => relay(params, requestOptions, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof relay>>,
TError,
TData
> & { queryKey: DataTag<QueryKey, TData, TError> };
};
export type RelayQueryResult = NonNullable<Awaited<ReturnType<typeof relay>>>;
export type RelayQueryError = HTTPValidationError;
export function useRelay<
TData = Awaited<ReturnType<typeof relay>>,
TError = HTTPValidationError,
>(
params: RelayParams,
options: {
query: Partial<
UseQueryOptions<Awaited<ReturnType<typeof relay>>, TError, TData>
> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof relay>>,
TError,
Awaited<ReturnType<typeof relay>>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useRelay<
TData = Awaited<ReturnType<typeof relay>>,
TError = HTTPValidationError,
>(
params: RelayParams,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof relay>>, TError, TData>
> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof relay>>,
TError,
Awaited<ReturnType<typeof relay>>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useRelay<
TData = Awaited<ReturnType<typeof relay>>,
TError = HTTPValidationError,
>(
params: RelayParams,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof relay>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/**
* @summary 사진 중계 — 캔버스 오염을 피하려고 같은 오리진으로 흘려보낸다
*/
export function useRelay<
TData = Awaited<ReturnType<typeof relay>>,
TError = HTTPValidationError,
>(
params: RelayParams,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof relay>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions = getRelayQueryOptions(params, options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = queryOptions.queryKey;
return query;
}

View File

@ -4,10 +4,7 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import { import { useMutation, useQuery } from "@tanstack/react-query";
useMutation,
useQuery
} from '@tanstack/react-query';
import type { import type {
DataTag, DataTag,
DefinedInitialDataOptions, DefinedInitialDataOptions,
@ -20,268 +17,374 @@ import type {
UseMutationOptions, UseMutationOptions,
UseMutationResult, UseMutationResult,
UseQueryOptions, UseQueryOptions,
UseQueryResult UseQueryResult,
} from '@tanstack/react-query'; } from "@tanstack/react-query";
import type { import type { HTTPValidationError, ResJob, ResJobOps } from ".././model";
HTTPValidationError,
ResJob,
ResJobOps
} from '.././model';
import { customFetch } from '../../mutator/custom-fetch';
import { customFetch } from "../../mutator/custom-fetch";
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1]; type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/** /**
* 상태별 카운트 + 큐 지연 + 최근 1시간 DEAD + 좀비(stuck). 내부 운영 전용. * 상태별 카운트 + 큐 지연 + 최근 1시간 DEAD + 좀비(stuck). 내부 운영 전용.
* @summary 큐 운영 스냅샷 * @summary 큐 운영 스냅샷
*/ */
export const jobOps = ( export const jobOps = (
options?: SecondParameter<typeof customFetch>,
options?: SecondParameter<typeof customFetch>,signal?: AbortSignal signal?: AbortSignal,
) => { ) => {
return customFetch<ResJobOps>(
{ url: `/v1/job/ops`, method: "GET", signal },
return customFetch<ResJobOps>( options,
{url: `/v1/job/ops`, method: 'GET', signal );
}, };
options);
}
export const getJobOpsQueryKey = () => { export const getJobOpsQueryKey = () => {
return [ return [`/v1/job/ops`] as const;
`/v1/job/ops` };
] as const;
}
export const getJobOpsQueryOptions = <
export const getJobOpsQueryOptions = <TData = Awaited<ReturnType<typeof jobOps>>, TError = void>( options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof jobOps>>, TError, TData>>, request?: SecondParameter<typeof customFetch>} TData = Awaited<ReturnType<typeof jobOps>>,
) => { TError = void,
>(options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof jobOps>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
}) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const {query: queryOptions, request: requestOptions} = options ?? {}; const queryKey = queryOptions?.queryKey ?? getJobOpsQueryKey();
const queryKey = queryOptions?.queryKey ?? getJobOpsQueryKey(); const queryFn: QueryFunction<Awaited<ReturnType<typeof jobOps>>> = ({
signal,
}) => jobOps(requestOptions, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof jobOps>>,
TError,
TData
> & { queryKey: DataTag<QueryKey, TData, TError> };
};
const queryFn: QueryFunction<Awaited<ReturnType<typeof jobOps>>> = ({ signal }) => jobOps(requestOptions, signal); export type JobOpsQueryResult = NonNullable<Awaited<ReturnType<typeof jobOps>>>;
export type JobOpsQueryError = void;
export function useJobOps<
TData = Awaited<ReturnType<typeof jobOps>>,
TError = void,
>(
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof jobOps>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> } options: {
} query: Partial<
UseQueryOptions<Awaited<ReturnType<typeof jobOps>>, TError, TData>
export type JobOpsQueryResult = NonNullable<Awaited<ReturnType<typeof jobOps>>> > &
export type JobOpsQueryError = void Pick<
export function useJobOps<TData = Awaited<ReturnType<typeof jobOps>>, TError = void>(
options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof jobOps>>, TError, TData>> & Pick<
DefinedInitialDataOptions< DefinedInitialDataOptions<
Awaited<ReturnType<typeof jobOps>>, Awaited<ReturnType<typeof jobOps>>,
TError, TError,
Awaited<ReturnType<typeof jobOps>> Awaited<ReturnType<typeof jobOps>>
> , 'initialData' >,
>, request?: SecondParameter<typeof customFetch>} "initialData"
, queryClient?: QueryClient >;
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } request?: SecondParameter<typeof customFetch>;
export function useJobOps<TData = Awaited<ReturnType<typeof jobOps>>, TError = void>( },
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof jobOps>>, TError, TData>> & Pick< queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useJobOps<
TData = Awaited<ReturnType<typeof jobOps>>,
TError = void,
>(
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof jobOps>>, TError, TData>
> &
Pick<
UndefinedInitialDataOptions< UndefinedInitialDataOptions<
Awaited<ReturnType<typeof jobOps>>, Awaited<ReturnType<typeof jobOps>>,
TError, TError,
Awaited<ReturnType<typeof jobOps>> Awaited<ReturnType<typeof jobOps>>
> , 'initialData' >,
>, request?: SecondParameter<typeof customFetch>} "initialData"
, queryClient?: QueryClient >;
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } request?: SecondParameter<typeof customFetch>;
export function useJobOps<TData = Awaited<ReturnType<typeof jobOps>>, TError = void>( },
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof jobOps>>, TError, TData>>, request?: SecondParameter<typeof customFetch>} queryClient?: QueryClient,
, queryClient?: QueryClient ): UseQueryResult<TData, TError> & {
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } queryKey: DataTag<QueryKey, TData, TError>;
};
export function useJobOps<
TData = Awaited<ReturnType<typeof jobOps>>,
TError = void,
>(
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof jobOps>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/** /**
* @summary 큐 운영 스냅샷 * @summary 큐 운영 스냅샷
*/ */
export function useJobOps<TData = Awaited<ReturnType<typeof jobOps>>, TError = void>( export function useJobOps<
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof jobOps>>, TError, TData>>, request?: SecondParameter<typeof customFetch>} TData = Awaited<ReturnType<typeof jobOps>>,
, queryClient?: QueryClient TError = void,
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } { >(
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof jobOps>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions = getJobOpsQueryOptions(options);
const queryOptions = getJobOpsQueryOptions(options) const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }; query.queryKey = queryOptions.queryKey;
query.queryKey = queryOptions.queryKey ;
return query; return query;
} }
/** /**
* @summary 잡 상태 조회(폴링) * @summary 잡 상태 조회(폴링)
*/ */
export const getJob = ( export const getJob = (
jobId: string, jobId: string,
options?: SecondParameter<typeof customFetch>,signal?: AbortSignal options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => { ) => {
return customFetch<ResJob>(
{ url: `/v1/job/${jobId}`, method: "GET", signal },
return customFetch<ResJob>( options,
{url: `/v1/job/${jobId}`, method: 'GET', signal );
}, };
options);
}
export const getGetJobQueryKey = (jobId?: string) => {
return [`/v1/job/${jobId}`] as const;
};
export const getGetJobQueryOptions = <
export const getGetJobQueryKey = (jobId?: string,) => { TData = Awaited<ReturnType<typeof getJob>>,
return [ TError = void | HTTPValidationError,
`/v1/job/${jobId}` >(
] as const; jobId: string,
} options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData>
export const getGetJobQueryOptions = <TData = Awaited<ReturnType<typeof getJob>>, TError = void | HTTPValidationError>(jobId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData>>, request?: SecondParameter<typeof customFetch>} >;
request?: SecondParameter<typeof customFetch>;
},
) => { ) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const {query: queryOptions, request: requestOptions} = options ?? {}; const queryKey = queryOptions?.queryKey ?? getGetJobQueryKey(jobId);
const queryKey = queryOptions?.queryKey ?? getGetJobQueryKey(jobId); const queryFn: QueryFunction<Awaited<ReturnType<typeof getJob>>> = ({
signal,
}) => getJob(jobId, requestOptions, signal);
return {
queryKey,
queryFn,
enabled: !!jobId,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
};
const queryFn: QueryFunction<Awaited<ReturnType<typeof getJob>>> = ({ signal }) => getJob(jobId, requestOptions, signal); export type GetJobQueryResult = NonNullable<Awaited<ReturnType<typeof getJob>>>;
export type GetJobQueryError = void | HTTPValidationError;
export function useGetJob<
TData = Awaited<ReturnType<typeof getJob>>,
TError = void | HTTPValidationError,
>(
return { queryKey, queryFn, enabled: !!(jobId), ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> } jobId: string,
} options: {
query: Partial<
export type GetJobQueryResult = NonNullable<Awaited<ReturnType<typeof getJob>>> UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData>
export type GetJobQueryError = void | HTTPValidationError > &
Pick<
export function useGetJob<TData = Awaited<ReturnType<typeof getJob>>, TError = void | HTTPValidationError>(
jobId: string, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData>> & Pick<
DefinedInitialDataOptions< DefinedInitialDataOptions<
Awaited<ReturnType<typeof getJob>>, Awaited<ReturnType<typeof getJob>>,
TError, TError,
Awaited<ReturnType<typeof getJob>> Awaited<ReturnType<typeof getJob>>
> , 'initialData' >,
>, request?: SecondParameter<typeof customFetch>} "initialData"
, queryClient?: QueryClient >;
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } request?: SecondParameter<typeof customFetch>;
export function useGetJob<TData = Awaited<ReturnType<typeof getJob>>, TError = void | HTTPValidationError>( },
jobId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData>> & Pick< queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useGetJob<
TData = Awaited<ReturnType<typeof getJob>>,
TError = void | HTTPValidationError,
>(
jobId: string,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData>
> &
Pick<
UndefinedInitialDataOptions< UndefinedInitialDataOptions<
Awaited<ReturnType<typeof getJob>>, Awaited<ReturnType<typeof getJob>>,
TError, TError,
Awaited<ReturnType<typeof getJob>> Awaited<ReturnType<typeof getJob>>
> , 'initialData' >,
>, request?: SecondParameter<typeof customFetch>} "initialData"
, queryClient?: QueryClient >;
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } request?: SecondParameter<typeof customFetch>;
export function useGetJob<TData = Awaited<ReturnType<typeof getJob>>, TError = void | HTTPValidationError>( },
jobId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData>>, request?: SecondParameter<typeof customFetch>} queryClient?: QueryClient,
, queryClient?: QueryClient ): UseQueryResult<TData, TError> & {
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } queryKey: DataTag<QueryKey, TData, TError>;
};
export function useGetJob<
TData = Awaited<ReturnType<typeof getJob>>,
TError = void | HTTPValidationError,
>(
jobId: string,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/** /**
* @summary 잡 상태 조회(폴링) * @summary 잡 상태 조회(폴링)
*/ */
export function useGetJob<TData = Awaited<ReturnType<typeof getJob>>, TError = void | HTTPValidationError>( export function useGetJob<
jobId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData>>, request?: SecondParameter<typeof customFetch>} TData = Awaited<ReturnType<typeof getJob>>,
, queryClient?: QueryClient TError = void | HTTPValidationError,
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } { >(
jobId: string,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions = getGetJobQueryOptions(jobId, options);
const queryOptions = getGetJobQueryOptions(jobId,options) const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }; query.queryKey = queryOptions.queryKey;
query.queryKey = queryOptions.queryKey ;
return query; return query;
} }
/** /**
* max_attempts 를 소진해 dead-letter 로 간 잡을 다시 대기로 돌린다. 내부 운영 전용. * max_attempts 를 소진해 dead-letter 로 간 잡을 다시 대기로 돌린다. 내부 운영 전용.
* @summary DEAD 잡 재큐 * @summary DEAD 잡 재큐
*/ */
export const requeueJob = ( export const requeueJob = (
jobId: string, jobId: string,
options?: SecondParameter<typeof customFetch>,signal?: AbortSignal options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => { ) => {
return customFetch<ResJob>(
{ url: `/v1/job/${jobId}/requeue`, method: "POST", signal },
return customFetch<ResJob>( options,
{url: `/v1/job/${jobId}/requeue`, method: 'POST', signal );
}, };
options);
}
export const getRequeueJobMutationOptions = <
TError = void | HTTPValidationError,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof requeueJob>>,
TError,
{ jobId: string },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof requeueJob>>,
TError,
{ jobId: string },
TContext
> => {
const mutationKey = ["requeueJob"];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
export const getRequeueJobMutationOptions = <TError = void | HTTPValidationError, const mutationFn: MutationFunction<
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof requeueJob>>, TError,{jobId: string}, TContext>, request?: SecondParameter<typeof customFetch>} Awaited<ReturnType<typeof requeueJob>>,
): UseMutationOptions<Awaited<ReturnType<typeof requeueJob>>, TError,{jobId: string}, TContext> => { { jobId: string }
> = (props) => {
const { jobId } = props ?? {};
const mutationKey = ['requeueJob']; return requeueJob(jobId, requestOptions);
const {mutation: mutationOptions, request: requestOptions} = options ? };
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
return { mutationFn, ...mutationOptions };
};
export type RequeueJobMutationResult = NonNullable<
Awaited<ReturnType<typeof requeueJob>>
>;
const mutationFn: MutationFunction<Awaited<ReturnType<typeof requeueJob>>, {jobId: string}> = (props) => { export type RequeueJobMutationError = void | HTTPValidationError;
const {jobId} = props ?? {};
return requeueJob(jobId,requestOptions) /**
}
return { mutationFn, ...mutationOptions }}
export type RequeueJobMutationResult = NonNullable<Awaited<ReturnType<typeof requeueJob>>>
export type RequeueJobMutationError = void | HTTPValidationError
/**
* @summary DEAD 잡 재큐 * @summary DEAD 잡 재큐
*/ */
export const useRequeueJob = <TError = void | HTTPValidationError, export const useRequeueJob = <
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof requeueJob>>, TError,{jobId: string}, TContext>, request?: SecondParameter<typeof customFetch>} TError = void | HTTPValidationError,
, queryClient?: QueryClient): UseMutationResult< TContext = unknown,
Awaited<ReturnType<typeof requeueJob>>, >(
TError, options?: {
{jobId: string}, mutation?: UseMutationOptions<
TContext Awaited<ReturnType<typeof requeueJob>>,
> => { TError,
{ jobId: string },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof requeueJob>>,
TError,
{ jobId: string },
TContext
> => {
const mutationOptions = getRequeueJobMutationOptions(options);
const mutationOptions = getRequeueJobMutationOptions(options); return useMutation(mutationOptions, queryClient);
};
return useMutation(mutationOptions, queryClient);
}

View File

@ -4,9 +4,7 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import { import { useQuery } from "@tanstack/react-query";
useQuery
} from '@tanstack/react-query';
import type { import type {
DataTag, DataTag,
DefinedInitialDataOptions, DefinedInitialDataOptions,
@ -16,120 +14,174 @@ import type {
QueryKey, QueryKey,
UndefinedInitialDataOptions, UndefinedInitialDataOptions,
UseQueryOptions, UseQueryOptions,
UseQueryResult UseQueryResult,
} from '@tanstack/react-query'; } from "@tanstack/react-query";
import type { import type {
HTTPValidationError, HTTPValidationError,
ListMediaParams, ListMediaParams,
ResMediaList ResMediaList,
} from '.././model'; } from ".././model";
import { customFetch } from '../../mutator/custom-fetch';
import { customFetch } from "../../mutator/custom-fetch";
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1]; type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/** /**
* publishable_only=true 면 ★ 사이트에 실제로 나갈 수 있는 것(승인 + alt 있음)만 — 빌드 스냅샷과 같은 기준이다. unit_id 를 주면 그 객실·메뉴 사진만. ★ 응답에는 항상 source_type(owner/crawl)과 origin_url 이 실린다 — 크롤링 이미지 재게시 권리가 미결이라(docs/DECISIONS.md 1-2) 결론에 따라 CRAWL 을 통째로 걸러내야 하고, 화면이 출처를 모르면 그 판단을 못 한다. * publishable_only=true 면 ★ 사이트에 실제로 나갈 수 있는 것(승인 + alt 있음)만 — 빌드 스냅샷과 같은 기준이다. unit_id 를 주면 그 객실·메뉴 사진만. ★ 응답에는 항상 source_type(owner/crawl)과 origin_url 이 실린다 — 크롤링 이미지 재게시 권리가 미결이라(docs/DECISIONS.md 1-2) 결론에 따라 CRAWL 을 통째로 걸러내야 하고, 화면이 출처를 모르면 그 판단을 못 한다.
* @summary 사진 목록 * @summary 사진 목록
*/ */
export const listMedia = ( export const listMedia = (
placeId: string, placeId: string,
params?: ListMediaParams, params?: ListMediaParams,
options?: SecondParameter<typeof customFetch>,signal?: AbortSignal options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => { ) => {
return customFetch<ResMediaList>(
{ url: `/v1/place/${placeId}/media/list`, method: "GET", params, signal },
return customFetch<ResMediaList>( options,
{url: `/v1/place/${placeId}/media/list`, method: 'GET', );
params, signal };
},
options);
}
export const getListMediaQueryKey = (
placeId?: string,
export const getListMediaQueryKey = (placeId?: string, params?: ListMediaParams,
params?: ListMediaParams,) => {
return [
`/v1/place/${placeId}/media/list`, ...(params ? [params]: [])
] as const;
}
export const getListMediaQueryOptions = <TData = Awaited<ReturnType<typeof listMedia>>, TError = void | HTTPValidationError>(placeId: string,
params?: ListMediaParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listMedia>>, TError, TData>>, request?: SecondParameter<typeof customFetch>}
) => { ) => {
return [
`/v1/place/${placeId}/media/list`,
...(params ? [params] : []),
] as const;
};
const {query: queryOptions, request: requestOptions} = options ?? {}; export const getListMediaQueryOptions = <
TData = Awaited<ReturnType<typeof listMedia>>,
TError = void | HTTPValidationError,
>(
placeId: string,
params?: ListMediaParams,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listMedia>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListMediaQueryKey(placeId,params); const queryKey =
queryOptions?.queryKey ?? getListMediaQueryKey(placeId, params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listMedia>>> = ({
signal,
}) => listMedia(placeId, params, requestOptions, signal);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listMedia>>> = ({ signal }) => listMedia(placeId,params, requestOptions, signal); return {
queryKey,
queryFn,
enabled: !!placeId,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof listMedia>>, TError, TData> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
};
export type ListMediaQueryResult = NonNullable<
Awaited<ReturnType<typeof listMedia>>
>;
export type ListMediaQueryError = void | HTTPValidationError;
export function useListMedia<
TData = Awaited<ReturnType<typeof listMedia>>,
return { queryKey, queryFn, enabled: !!(placeId), ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof listMedia>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> } TError = void | HTTPValidationError,
} >(
placeId: string,
export type ListMediaQueryResult = NonNullable<Awaited<ReturnType<typeof listMedia>>> params: undefined | ListMediaParams,
export type ListMediaQueryError = void | HTTPValidationError options: {
query: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listMedia>>, TError, TData>
export function useListMedia<TData = Awaited<ReturnType<typeof listMedia>>, TError = void | HTTPValidationError>( > &
placeId: string, Pick<
params: undefined | ListMediaParams, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof listMedia>>, TError, TData>> & Pick<
DefinedInitialDataOptions< DefinedInitialDataOptions<
Awaited<ReturnType<typeof listMedia>>, Awaited<ReturnType<typeof listMedia>>,
TError, TError,
Awaited<ReturnType<typeof listMedia>> Awaited<ReturnType<typeof listMedia>>
> , 'initialData' >,
>, request?: SecondParameter<typeof customFetch>} "initialData"
, queryClient?: QueryClient >;
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } request?: SecondParameter<typeof customFetch>;
export function useListMedia<TData = Awaited<ReturnType<typeof listMedia>>, TError = void | HTTPValidationError>( },
placeId: string, queryClient?: QueryClient,
params?: ListMediaParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listMedia>>, TError, TData>> & Pick< ): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useListMedia<
TData = Awaited<ReturnType<typeof listMedia>>,
TError = void | HTTPValidationError,
>(
placeId: string,
params?: ListMediaParams,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listMedia>>, TError, TData>
> &
Pick<
UndefinedInitialDataOptions< UndefinedInitialDataOptions<
Awaited<ReturnType<typeof listMedia>>, Awaited<ReturnType<typeof listMedia>>,
TError, TError,
Awaited<ReturnType<typeof listMedia>> Awaited<ReturnType<typeof listMedia>>
> , 'initialData' >,
>, request?: SecondParameter<typeof customFetch>} "initialData"
, queryClient?: QueryClient >;
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } request?: SecondParameter<typeof customFetch>;
export function useListMedia<TData = Awaited<ReturnType<typeof listMedia>>, TError = void | HTTPValidationError>( },
placeId: string, queryClient?: QueryClient,
params?: ListMediaParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listMedia>>, TError, TData>>, request?: SecondParameter<typeof customFetch>} ): UseQueryResult<TData, TError> & {
, queryClient?: QueryClient queryKey: DataTag<QueryKey, TData, TError>;
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } };
export function useListMedia<
TData = Awaited<ReturnType<typeof listMedia>>,
TError = void | HTTPValidationError,
>(
placeId: string,
params?: ListMediaParams,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listMedia>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/** /**
* @summary 사진 목록 * @summary 사진 목록
*/ */
export function useListMedia<TData = Awaited<ReturnType<typeof listMedia>>, TError = void | HTTPValidationError>( export function useListMedia<
placeId: string, TData = Awaited<ReturnType<typeof listMedia>>,
params?: ListMediaParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listMedia>>, TError, TData>>, request?: SecondParameter<typeof customFetch>} TError = void | HTTPValidationError,
, queryClient?: QueryClient >(
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } { placeId: string,
params?: ListMediaParams,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listMedia>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions = getListMediaQueryOptions(placeId, params, options);
const queryOptions = getListMediaQueryOptions(placeId,params,options) const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }; query.queryKey = queryOptions.queryKey;
query.queryKey = queryOptions.queryKey ;
return query; return query;
} }

View File

@ -4,7 +4,7 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { AuditCheckDataRecommendation } from './auditCheckDataRecommendation'; import type { AuditCheckDataRecommendation } from "./auditCheckDataRecommendation";
export interface AuditCheckData { export interface AuditCheckData {
id: string; id: string;

View File

@ -12,8 +12,7 @@
이으려면 "먼저 가입한 쪽의 소유"를 증명받아야 하는데, 그 증명 없이 이메일만 보고 이으면 이으려면 "먼저 가입한 쪽의 소유"를 증명받아야 하는데, 그 증명 없이 이메일만 보고 이으면
남이 먼저 만들어 둔 계정에 내 구글 로그인이 들어간다(계정 선점). 보류 사유는 DECISIONS.md 1절. 남이 먼저 만들어 둔 계정에 내 구글 로그인이 들어간다(계정 선점). 보류 사유는 DECISIONS.md 1절.
*/ */
export type AuthProvider = typeof AuthProvider[keyof typeof AuthProvider]; export type AuthProvider = (typeof AuthProvider)[keyof typeof AuthProvider];
// eslint-disable-next-line @typescript-eslint/no-redeclare // eslint-disable-next-line @typescript-eslint/no-redeclare
export const AuthProvider = { export const AuthProvider = {

View File

@ -8,8 +8,7 @@
/** /**
* site_versions.build_status 코드값. 정적 빌드는 개별 재빌드 단위로 돈다. * site_versions.build_status 코드값. 정적 빌드는 개별 재빌드 단위로 돈다.
*/ */
export type BuildStatus = typeof BuildStatus[keyof typeof BuildStatus]; export type BuildStatus = (typeof BuildStatus)[keyof typeof BuildStatus];
// eslint-disable-next-line @typescript-eslint/no-redeclare // eslint-disable-next-line @typescript-eslint/no-redeclare
export const BuildStatus = { export const BuildStatus = {

View File

@ -6,8 +6,8 @@
*/ */
export type CheckSlugParams = { export type CheckSlugParams = {
/** /**
* 쓰고 싶은 주소(예: doflo) * 쓰고 싶은 주소(예: doflo)
*/ */
slug: string; slug: string;
}; };

View File

@ -4,9 +4,9 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { ErrorInfoSuccess } from './errorInfoSuccess'; import type { ErrorInfoSuccess } from "./errorInfoSuccess";
import type { ErrorInfoCode } from './errorInfoCode'; import type { ErrorInfoCode } from "./errorInfoCode";
import type { ErrorInfoDesc } from './errorInfoDesc'; import type { ErrorInfoDesc } from "./errorInfoDesc";
/** /**
* 모든 응답에 공통으로 실리는 결과 정보. result.success / code / desc 로 내려간다. * 모든 응답에 공통으로 실리는 결과 정보. result.success / code / desc 로 내려간다.

View File

@ -11,8 +11,8 @@
카카오는 안정적인 고유 place id 를 준다 → 그걸로 중복 등록을 막는다. 카카오는 안정적인 고유 place id 를 준다 → 그걸로 중복 등록을 막는다.
네이버는 고유 id 가 없다(응답의 link 는 업체 홈페이지다) → 상호명+도로명주소로 막는다. 네이버는 고유 id 가 없다(응답의 link 는 업체 홈페이지다) → 상호명+도로명주소로 막는다.
*/ */
export type ExternalPlaceSource = typeof ExternalPlaceSource[keyof typeof ExternalPlaceSource]; export type ExternalPlaceSource =
(typeof ExternalPlaceSource)[keyof typeof ExternalPlaceSource];
// eslint-disable-next-line @typescript-eslint/no-redeclare // eslint-disable-next-line @typescript-eslint/no-redeclare
export const ExternalPlaceSource = { export const ExternalPlaceSource = {

View File

@ -4,17 +4,17 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { FactDataUnitId } from './factDataUnitId'; import type { FactDataUnitId } from "./factDataUnitId";
import type { FactDataValue } from './factDataValue'; import type { FactDataValue } from "./factDataValue";
import type { FactDataSummary } from './factDataSummary'; import type { FactDataSummary } from "./factDataSummary";
import type { FactDataUnit } from './factDataUnit'; import type { FactDataUnit } from "./factDataUnit";
import type { SourceType } from './sourceType'; import type { SourceType } from "./sourceType";
import type { FactDataSourceUrl } from './factDataSourceUrl'; import type { FactDataSourceUrl } from "./factDataSourceUrl";
import type { FactDataCollectedAt } from './factDataCollectedAt'; import type { FactDataCollectedAt } from "./factDataCollectedAt";
import type { FactDataVerifiedBy } from './factDataVerifiedBy'; import type { FactDataVerifiedBy } from "./factDataVerifiedBy";
import type { FactDataVerifiedAt } from './factDataVerifiedAt'; import type { FactDataVerifiedAt } from "./factDataVerifiedAt";
import type { FactStatus } from './factStatus'; import type { FactStatus } from "./factStatus";
import type { FactDataExpiresAt } from './factDataExpiresAt'; import type { FactDataExpiresAt } from "./factDataExpiresAt";
export interface FactData { export interface FactData {
fact_id: string; fact_id: string;

View File

@ -9,8 +9,7 @@
* facts.status / faqs.status / routes.status 공용 검증 상태. * facts.status / faqs.status / routes.status 공용 검증 상태.
★ VERIFIED 와 CORRECTED 만 사이트에 노출한다(PUBLISHABLE_FACT_STATUSES). ★ VERIFIED 와 CORRECTED 만 사이트에 노출한다(PUBLISHABLE_FACT_STATUSES).
*/ */
export type FactStatus = typeof FactStatus[keyof typeof FactStatus]; export type FactStatus = (typeof FactStatus)[keyof typeof FactStatus];
// eslint-disable-next-line @typescript-eslint/no-redeclare // eslint-disable-next-line @typescript-eslint/no-redeclare
export const FactStatus = { export const FactStatus = {

View File

@ -9,8 +9,8 @@
* fact 기록 결과. 재수집(업데이트)이 무엇을 했는지 호출측이 알아야 한다 — * fact 기록 결과. 재수집(업데이트)이 무엇을 했는지 호출측이 알아야 한다 —
특히 사이트 재빌드가 필요한 경우(PUBLISHED_REPLACED)를 구분해야 한다. 특히 사이트 재빌드가 필요한 경우(PUBLISHED_REPLACED)를 구분해야 한다.
*/ */
export type FactWriteOutcome = typeof FactWriteOutcome[keyof typeof FactWriteOutcome]; export type FactWriteOutcome =
(typeof FactWriteOutcome)[keyof typeof FactWriteOutcome];
// eslint-disable-next-line @typescript-eslint/no-redeclare // eslint-disable-next-line @typescript-eslint/no-redeclare
export const FactWriteOutcome = { export const FactWriteOutcome = {

View File

@ -4,11 +4,11 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { FaqDataSourceFactIds } from './faqDataSourceFactIds'; import type { FaqDataSourceFactIds } from "./faqDataSourceFactIds";
import type { SourceType } from './sourceType'; import type { SourceType } from "./sourceType";
import type { FactStatus } from './factStatus'; import type { FactStatus } from "./factStatus";
import type { FaqDataCreatedAt } from './faqDataCreatedAt'; import type { FaqDataCreatedAt } from "./faqDataCreatedAt";
import type { FaqDataUpdatedAt } from './faqDataUpdatedAt'; import type { FaqDataUpdatedAt } from "./faqDataUpdatedAt";
export interface FaqData { export interface FaqData {
faq_id: string; faq_id: string;

View File

@ -4,7 +4,7 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { FieldSpecDataUnit } from './fieldSpecDataUnit'; import type { FieldSpecDataUnit } from "./fieldSpecDataUnit";
/** /**
* 업종 스키마의 필드 정의 — 관리 화면이 입력 폼을 그리는 데 쓴다. * 업종 스키마의 필드 정의 — 관리 화면이 입력 폼을 그리는 데 쓴다.

View File

@ -5,4 +5,6 @@
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
export type ReqSyncFestivalsAreaCode = string | null; export type GetGuideParams = {
place_id: string;
};

View File

@ -6,19 +6,19 @@
*/ */
export type GetWeatherParams = { export type GetWeatherParams = {
/** /**
* @minLength 1 * @minLength 1
* @maxLength 10 * @maxLength 10
*/ */
region_code: string; region_code: string;
/** /**
* @minimum -90 * @minimum -90
* @maximum 90 * @maximum 90
*/ */
latitude: number; latitude: number;
/** /**
* @minimum -180 * @minimum -180
* @maximum 180 * @maximum 180
*/ */
longitude: number; longitude: number;
}; };

View File

@ -4,7 +4,7 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { ValidationError } from './validationError'; import type { ValidationError } from "./validationError";
export interface HTTPValidationError { export interface HTTPValidationError {
detail?: ValidationError[]; detail?: ValidationError[];

View File

@ -5,318 +5,333 @@
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
export * from './auditCheckData'; export * from "./auditCheckData";
export * from './auditCheckDataRecommendation'; export * from "./auditCheckDataRecommendation";
export * from './authProvider'; export * from "./authProvider";
export * from './buildStatus'; export * from "./buildStatus";
export * from './checkSlugParams'; export * from "./checkSlugParams";
export * from './errorInfo'; export * from "./errorInfo";
export * from './errorInfoCode'; export * from "./errorInfoCode";
export * from './errorInfoDesc'; export * from "./errorInfoDesc";
export * from './errorInfoSuccess'; export * from "./errorInfoSuccess";
export * from './externalPlaceSource'; export * from "./externalPlaceSource";
export * from './factData'; export * from "./factData";
export * from './factDataCollectedAt'; export * from "./factDataCollectedAt";
export * from './factDataExpiresAt'; export * from "./factDataExpiresAt";
export * from './factDataSourceUrl'; export * from "./factDataSourceUrl";
export * from './factDataSummary'; export * from "./factDataSummary";
export * from './factDataUnit'; export * from "./factDataUnit";
export * from './factDataUnitId'; export * from "./factDataUnitId";
export * from './factDataValue'; export * from "./factDataValue";
export * from './factDataVerifiedAt'; export * from "./factDataVerifiedAt";
export * from './factDataVerifiedBy'; export * from "./factDataVerifiedBy";
export * from './factStatus'; export * from "./factStatus";
export * from './factWriteOutcome'; export * from "./factWriteOutcome";
export * from './faqData'; export * from "./faqData";
export * from './faqDataCreatedAt'; export * from "./faqDataCreatedAt";
export * from './faqDataSourceFactIds'; export * from "./faqDataSourceFactIds";
export * from './faqDataUpdatedAt'; export * from "./faqDataUpdatedAt";
export * from './fieldSpecData'; export * from "./fieldSpecData";
export * from './fieldSpecDataUnit'; export * from "./fieldSpecDataUnit";
export * from './getWeatherParams'; export * from "./getGuideParams";
export * from './hTTPValidationError'; export * from "./getWeatherParams";
export * from './jobData'; export * from "./hTTPValidationError";
export * from './jobDataCreatedAt'; export * from "./jobData";
export * from './jobDataLastError'; export * from "./jobDataCreatedAt";
export * from './jobDataResult'; export * from "./jobDataLastError";
export * from './jobDataRunAfter'; export * from "./jobDataResult";
export * from './jobDataRunStartedAt'; export * from "./jobDataRunAfter";
export * from './jobDataUpdatedAt'; export * from "./jobDataRunStartedAt";
export * from './jobStatus'; export * from "./jobDataUpdatedAt";
export * from './jobType'; export * from "./jobStatus";
export * from './linkChannel'; export * from "./jobType";
export * from './linkData'; export * from "./linkChannel";
export * from './linkDataConfirmedAt'; export * from "./linkData";
export * from './linkDataDiscoveredAt'; export * from "./linkDataConfirmedAt";
export * from './linkDataTitle'; export * from "./linkDataDiscoveredAt";
export * from './listContentsParams'; export * from "./linkDataTitle";
export * from './listFactsParams'; export * from "./listContentsParams";
export * from './listFaqsParams'; export * from "./listFactsParams";
export * from './listLinksParams'; export * from "./listFaqsParams";
export * from './listMediaParams'; export * from "./listLinksParams";
export * from './listMySitesParams'; export * from "./listMediaParams";
export * from './listPlacesParams'; export * from "./listMySitesParams";
export * from './listShowcaseParams'; export * from "./listPlacesParams";
export * from './localContentData'; export * from "./listShowcaseParams";
export * from './localContentDataBody'; export * from "./localContentData";
export * from './localContentDataCollectedAt'; export * from "./localContentDataBody";
export * from './localContentDataDisplayEndAt'; export * from "./localContentDataCollectedAt";
export * from './localContentDataDisplayStartAt'; export * from "./localContentDataDisplayEndAt";
export * from './localContentDataExternalId'; export * from "./localContentDataDisplayStartAt";
export * from './localContentDataPublishedAt'; export * from "./localContentDataExternalId";
export * from './localContentDataTitle'; export * from "./localContentDataPublishedAt";
export * from './localContentStatus'; export * from "./localContentDataTitle";
export * from './localContentType'; export * from "./localContentStatus";
export * from './localSource'; export * from "./localContentType";
export * from './mediaData'; export * from "./localSource";
export * from './mediaDataAltText'; export * from "./mediaData";
export * from './mediaDataCreatedAt'; export * from "./mediaDataAltText";
export * from './mediaDataHeight'; export * from "./mediaDataCreatedAt";
export * from './mediaDataLabel'; export * from "./mediaDataHeight";
export * from './mediaDataOriginUrl'; export * from "./mediaDataLabel";
export * from './mediaDataSourceUrl'; export * from "./mediaDataOriginUrl";
export * from './mediaDataUnitId'; export * from "./mediaDataSourceUrl";
export * from './mediaDataVisionConfidence'; export * from "./mediaDataUnitId";
export * from './mediaDataWidth'; export * from "./mediaDataVisionConfidence";
export * from './mediaStatus'; export * from "./mediaDataWidth";
export * from './mySiteData'; export * from "./mediaStatus";
export * from './mySiteDataCreatedAt'; export * from "./mySiteData";
export * from './mySiteDataDomain'; export * from "./mySiteDataCreatedAt";
export * from './mySiteDataPublishedAt'; export * from "./mySiteDataDomain";
export * from './mySiteDataThumbnailUrl'; export * from "./mySiteDataPublishedAt";
export * from './mySiteDataRoadAddress'; export * from "./mySiteDataRoadAddress";
export * from './mySiteDataSiteId'; export * from "./mySiteDataSiteId";
export * from './mySiteDataStatus'; export * from "./mySiteDataStatus";
export * from './mySiteDataTemplateId'; export * from "./mySiteDataTemplateId";
export * from './placeCandidate'; export * from "./mySiteDataThumbnailUrl";
export * from './placeCandidateAddress'; export * from "./placeCandidate";
export * from './placeCandidateCategoryName'; export * from "./placeCandidateAddress";
export * from './placeCandidateExternalPlaceId'; export * from "./placeCandidateCategoryName";
export * from './placeCandidateLatitude'; export * from "./placeCandidateExternalPlaceId";
export * from './placeCandidateLongitude'; export * from "./placeCandidateLatitude";
export * from './placeCandidateNaverPlaceUrl'; export * from "./placeCandidateLongitude";
export * from './placeCandidatePhone'; export * from "./placeCandidateNaverPlaceUrl";
export * from './placeCandidatePlaceUrl'; export * from "./placeCandidatePhone";
export * from './placeCandidateRoadAddress'; export * from "./placeCandidatePlaceUrl";
export * from './placeCategory'; export * from "./placeCandidateRoadAddress";
export * from './placeData'; export * from "./placeCategory";
export * from './placeDataAddress'; export * from "./placeContentData";
export * from './placeDataContentUpdatedAt'; export * from "./placeContentDataBody";
export * from './placeDataCreatedAt'; export * from "./placeContentDataCollectedAt";
export * from './placeDataExternalPlaceId'; export * from "./placeContentDataDisplayEndAt";
export * from './placeDataExternalSource'; export * from "./placeData";
export * from './placeDataLatitude'; export * from "./placeDataAddress";
export * from './placeDataLongitude'; export * from "./placeDataContentUpdatedAt";
export * from './placeDataOwnerUserId'; export * from "./placeDataCreatedAt";
export * from './placeDataPhone'; export * from "./placeDataExternalPlaceId";
export * from './placeDataRegionCode'; export * from "./placeDataExternalSource";
export * from './placeDataRoadAddress'; export * from "./placeDataLatitude";
export * from './placeDataVerifiedAt'; export * from "./placeDataLongitude";
export * from './placeSearchItem'; export * from "./placeDataOwnerUserId";
export * from './placeSearchItemCategory'; export * from "./placeDataPhone";
export * from './placeSearchItemCategoryName'; export * from "./placeDataRegionCode";
export * from './placeSearchItemRoadAddress'; export * from "./placeDataRoadAddress";
export * from './placeStatus'; export * from "./placeDataVerifiedAt";
export * from './publishAction'; export * from "./placeSearchItem";
export * from './publishLogData'; export * from "./placeSearchItemCategory";
export * from './publishLogDataCreatedAt'; export * from "./placeSearchItemCategoryName";
export * from './publishLogDataDetail'; export * from "./placeSearchItemNaverPlaceUrl";
export * from './publishLogDataRejectReason'; export * from "./placeSearchItemRoadAddress";
export * from './publishRejectReason'; export * from "./placeStatus";
export * from './publishResult'; export * from "./publishAction";
export * from './renderStatusData'; export * from "./publishLogData";
export * from './renderStatusDataError'; export * from "./publishLogDataCreatedAt";
export * from './renderStatusDataRenderedAt'; export * from "./publishLogDataDetail";
export * from './renderStatusDataRenderedVersion'; export * from "./publishLogDataRejectReason";
export * from './reqCreateFaq'; export * from "./publishRejectReason";
export * from './reqCreateFaqSortOrder'; export * from "./publishResult";
export * from './reqCreateLink'; export * from "./relayParams";
export * from './reqCreateLinkTitle'; export * from "./renderStatusData";
export * from './reqCreatePlace'; export * from "./renderStatusDataError";
export * from './reqCreatePlaceOwnerUserId'; export * from "./renderStatusDataRenderedAt";
export * from './reqCreateUnit'; export * from "./renderStatusDataRenderedVersion";
export * from './reqExtractFacts'; export * from "./reqCreateFaq";
export * from './reqGoogleLogin'; export * from "./reqCreateFaqSortOrder";
export * from './reqLogin'; export * from "./reqCreateLink";
export * from './reqPublishLocalContent'; export * from "./reqCreateLinkTitle";
export * from './reqSignup'; export * from "./reqCreatePlace";
export * from './reqSignupName'; export * from "./reqCreateUnit";
export * from './reqSiteSlug'; export * from "./reqExtractFacts";
export * from './reqSiteStatus'; export * from "./reqGoogleLogin";
export * from './reqSiteTemplate'; export * from "./reqHidePlaceContent";
export * from './reqSiteTheme'; export * from "./reqLogin";
export * from './reqSiteThemeTheme'; export * from "./reqPublishLocalContent";
export * from './reqStartBuild'; export * from "./reqRollback";
export * from './reqStartCollect'; export * from "./reqSignup";
export * from './reqStartCopy'; export * from "./reqSignupName";
export * from './reqStartVision'; export * from "./reqSiteSlug";
export * from './reqSyncFestivals'; export * from "./reqSiteStatus";
export * from './reqSyncFestivalsAreaCode'; export * from "./reqSiteTemplate";
export * from './reqSyncFestivalsStartDate'; export * from "./reqSiteTheme";
export * from './reqTransitionFact'; export * from "./reqSiteThemeTheme";
export * from './reqTransitionFactValue'; export * from "./reqStartBuild";
export * from './reqTransitionFaq'; export * from "./reqStartCollect";
export * from './reqTransitionFaqAnswer'; export * from "./reqStartCopy";
export * from './reqTransitionFaqQuestion'; export * from "./reqStartVision";
export * from './reqUpdateLocalContent'; export * from "./reqTransitionFact";
export * from './reqUpdateLocalContentBody'; export * from "./reqTransitionFactValue";
export * from './reqUpdateLocalContentBodyAnyOf'; export * from "./reqTransitionFaq";
export * from './reqUpdateLocalContentDisplayEndAt'; export * from "./reqTransitionFaqAnswer";
export * from './reqUpdateLocalContentDisplayStartAt'; export * from "./reqTransitionFaqQuestion";
export * from './reqUpdateLocalContentTitle'; export * from "./reqUpdateLocalContent";
export * from './reqUpdateMe'; export * from "./reqUpdateLocalContentBody";
export * from './reqUpdateMeContactNumber'; export * from "./reqUpdateLocalContentBodyAnyOf";
export * from './reqUpdateMeEmail'; export * from "./reqUpdateLocalContentDisplayEndAt";
export * from './reqUpdateMeName'; export * from "./reqUpdateLocalContentDisplayStartAt";
export * from './reqUpdateMePassword'; export * from "./reqUpdateLocalContentTitle";
export * from './reqUpdatePlace'; export * from "./reqUpdateMe";
export * from './reqUpdatePlaceName'; export * from "./reqUpdateMeContactNumber";
export * from './reqUpdatePlaceOwnerUserId'; export * from "./reqUpdateMeEmail";
export * from './reqUpdatePlaceStatus'; export * from "./reqUpdateMeName";
export * from './reqUpsertFact'; export * from "./reqUpdateMePassword";
export * from './reqUpsertFactExpiresAt'; export * from "./reqUpdatePlace";
export * from './reqUpsertFactSourceUrl'; export * from "./reqUpdatePlaceName";
export * from './reqUpsertFactUnitId'; export * from "./reqUpdatePlaceStatus";
export * from './reqUpsertFactValue'; export * from "./reqUpsertFact";
export * from './reqVerifyPlace'; export * from "./reqUpsertFactExpiresAt";
export * from './reqVerifyPlaceAddress'; export * from "./reqUpsertFactSourceUrl";
export * from './reqVerifyPlaceByUrl'; export * from "./reqUpsertFactUnitId";
export * from './reqVerifyPlaceLatitude'; export * from "./reqUpsertFactValue";
export * from './reqVerifyPlaceLongitude'; export * from "./reqVerifyPlace";
export * from './reqVerifyPlacePhone'; export * from "./reqVerifyPlaceAddress";
export * from './reqVerifyPlacePlaceUrl'; export * from "./reqVerifyPlaceByUrl";
export * from './reqVerifyPlaceRegionCode'; export * from "./reqVerifyPlaceCategoryName";
export * from './reqVerifyPlaceRoadAddress'; export * from "./reqVerifyPlaceLatitude";
export * from './resCategorySchema'; export * from "./reqVerifyPlaceLongitude";
export * from './resCategorySchemaCategory'; export * from "./reqVerifyPlacePhone";
export * from './resCategorySchemaLabel'; export * from "./reqVerifyPlacePlaceUrl";
export * from './resCategorySchemaMsg'; export * from "./reqVerifyPlaceRegionCode";
export * from './resExtractFacts'; export * from "./reqVerifyPlaceRoadAddress";
export * from './resExtractFactsMsg'; export * from "./resCategorySchema";
export * from './resExtractedFact'; export * from "./resCategorySchemaCategory";
export * from './resExtractedFactReason'; export * from "./resCategorySchemaLabel";
export * from './resExtractedFactUnitName'; export * from "./resCategorySchemaMsg";
export * from './resFact'; export * from "./resExtractFacts";
export * from './resFactFact'; export * from "./resExtractFactsMsg";
export * from './resFactList'; export * from "./resExtractedFact";
export * from './resFactListMsg'; export * from "./resExtractedFactReason";
export * from './resFactMsg'; export * from "./resExtractedFactUnitName";
export * from './resFactOutcome'; export * from "./resFact";
export * from './resFaq'; export * from "./resFactFact";
export * from './resFaqFaq'; export * from "./resFactList";
export * from './resFaqList'; export * from "./resFactListMsg";
export * from './resFaqListMsg'; export * from "./resFactMsg";
export * from './resFaqMsg'; export * from "./resFactOutcome";
export * from './resJob'; export * from "./resFaq";
export * from './resJobJob'; export * from "./resFaqFaq";
export * from './resJobMsg'; export * from "./resFaqList";
export * from './resJobOps'; export * from "./resFaqListMsg";
export * from './resJobOpsMsg'; export * from "./resFaqMsg";
export * from './resLink'; export * from "./resJob";
export * from './resLinkLink'; export * from "./resJobJob";
export * from './resLinkList'; export * from "./resJobMsg";
export * from './resLinkListMsg'; export * from "./resJobOps";
export * from './resLinkMsg'; export * from "./resJobOpsMsg";
export * from './resLocalContentList'; export * from "./resLink";
export * from './resLocalContentListMsg'; export * from "./resLinkLink";
export * from './resLogin'; export * from "./resLinkList";
export * from './resLoginMsg'; export * from "./resLinkListMsg";
export * from './resMe'; export * from "./resLinkMsg";
export * from './resMeContactNumber'; export * from "./resLocalContentList";
export * from './resMeEmail'; export * from "./resLocalContentListMsg";
export * from './resMeMsg'; export * from "./resLocalGuide";
export * from './resMeName'; export * from "./resLocalGuideAttractionsItem";
export * from './resMediaList'; export * from "./resLocalGuideCoursesItem";
export * from './resMediaListMsg'; export * from "./resLocalGuideFestivalsItem";
export * from './resMySites'; export * from "./resLocalGuideItinerariesItem";
export * from './resMySitesMsg'; export * from "./resLocalGuideMsg";
export * from './resPlace'; export * from "./resLocalGuideRestaurantsItem";
export * from './resPlaceList'; export * from "./resLocalGuideSyncedAt";
export * from './resPlaceListMsg'; export * from "./resLogin";
export * from './resPlaceMsg'; export * from "./resLoginMsg";
export * from './resPlacePlace'; export * from "./resMe";
export * from './resPlaceSearch'; export * from "./resMeContactNumber";
export * from './resPlaceSearchMsg'; export * from "./resMeEmail";
export * from './resPlaceSearchSource'; export * from "./resMeMsg";
export * from './resPublishLogs'; export * from "./resMeName";
export * from './resPublishLogsMsg'; export * from "./resMediaList";
export * from './resRefreshToken'; export * from "./resMediaListMsg";
export * from './resRefreshTokenMsg'; export * from "./resMySites";
export * from './resSeoAudit'; export * from "./resMySitesMsg";
export * from './resSeoAuditMsg'; export * from "./resPlace";
export * from './resSeoAuditSummary'; export * from "./resPlaceContentList";
export * from './resSeoAuditVisibility'; export * from "./resPlaceContentListMsg";
export * from './resShowcase'; export * from "./resPlaceList";
export * from './resShowcaseMsg'; export * from "./resPlaceListMsg";
export * from './resSite'; export * from "./resPlaceMsg";
export * from './resSiteCurrentVersion'; export * from "./resPlacePlace";
export * from './resSiteMsg'; export * from "./resPlaceSearch";
export * from './resSiteSite'; export * from "./resPlaceSearchMsg";
export * from './resSiteSlug'; export * from "./resPlaceSearchSource";
export * from './resSiteSlugMsg'; export * from "./resPublishLogs";
export * from './resSiteSlugReason'; export * from "./resPublishLogsMsg";
export * from './resSiteSlugSite'; export * from "./resRefreshToken";
export * from './resSiteSlugSuggestion'; export * from "./resRefreshTokenMsg";
export * from './resSiteVersions'; export * from "./resSeoAudit";
export * from './resSiteVersionsMsg'; export * from "./resSeoAuditMsg";
export * from './resSlugCheck'; export * from "./resSeoAuditSummary";
export * from './resSlugCheckMsg'; export * from "./resSeoAuditVisibility";
export * from './resSlugCheckReason'; export * from "./resShowcase";
export * from './resSlugCheckSuggestion'; export * from "./resShowcaseMsg";
export * from './resStartBuild'; export * from "./resSite";
export * from './resStartBuildJobId'; export * from "./resSiteCurrentVersion";
export * from './resStartBuildMsg'; export * from "./resSiteMsg";
export * from './resStartBuildStatus'; export * from "./resSiteSite";
export * from './resStartCollect'; export * from "./resSiteSlug";
export * from './resStartCollectJobId'; export * from "./resSiteSlugMsg";
export * from './resStartCollectMsg'; export * from "./resSiteSlugReason";
export * from './resStartCollectStatus'; export * from "./resSiteSlugSite";
export * from './resStartCopy'; export * from "./resSiteSlugSuggestion";
export * from './resStartCopyJobId'; export * from "./resSiteVersions";
export * from './resStartCopyMsg'; export * from "./resSiteVersionsMsg";
export * from './resStartCopyStatus'; export * from "./resSlugCheck";
export * from './resStartVision'; export * from "./resSlugCheckMsg";
export * from './resStartVisionJobId'; export * from "./resSlugCheckReason";
export * from './resStartVisionMsg'; export * from "./resSlugCheckSuggestion";
export * from './resStartVisionStatus'; export * from "./resStartBuild";
export * from './resSyncFestivals'; export * from "./resStartBuildJobId";
export * from './resSyncFestivalsMsg'; export * from "./resStartBuildMsg";
export * from './resUnit'; export * from "./resStartBuildStatus";
export * from './resUnitList'; export * from "./resStartCollect";
export * from './resUnitListMsg'; export * from "./resStartCollectJobId";
export * from './resUnitMsg'; export * from "./resStartCollectMsg";
export * from './resUnitUnit'; export * from "./resStartCollectStatus";
export * from './resVerifyCandidates'; export * from "./resStartCopy";
export * from './resVerifyCandidatesMsg'; export * from "./resStartCopyJobId";
export * from './resVerifyCandidatesReason'; export * from "./resStartCopyMsg";
export * from './resVerifyCandidatesSource'; export * from "./resStartCopyStatus";
export * from './resWeather'; export * from "./resStartVision";
export * from './resWeatherMsg'; export * from "./resStartVisionJobId";
export * from './resWeatherWeather'; export * from "./resStartVisionMsg";
export * from './searchPlacesPublicParams'; export * from "./resStartVisionStatus";
export * from './showcaseItem'; export * from "./resSyncPlace";
export * from './showcaseItemRegion'; export * from "./resSyncPlaceMsg";
export * from './showcaseItemThumbnailUrl'; export * from "./resUnit";
export * from './siteData'; export * from "./resUnitList";
export * from './siteDataCurrentVersionId'; export * from "./resUnitListMsg";
export * from './siteDataDomain'; export * from "./resUnitMsg";
export * from './siteDataPublishedAt'; export * from "./resUnitUnit";
export * from './siteDataTemplateId'; export * from "./resVerifyCandidates";
export * from './siteDataTheme'; export * from "./resVerifyCandidatesMsg";
export * from './siteDataThemeAnyOf'; export * from "./resVerifyCandidatesReason";
export * from './siteDataThumbnailUrl'; export * from "./resVerifyCandidatesSource";
export * from './siteStatus'; export * from "./resWeather";
export * from './siteVersionData'; export * from "./resWeatherMsg";
export * from './siteVersionDataBuildError'; export * from "./resWeatherWeather";
export * from './siteVersionDataBuiltAt'; export * from "./searchPlacesPublicParams";
export * from './siteVersionDataCreatedAt'; export * from "./showcaseItem";
export * from './sourceType'; export * from "./showcaseItemRegion";
export * from './unitData'; export * from "./showcaseItemThumbnailUrl";
export * from './userRole'; export * from "./siteData";
export * from './validationError'; export * from "./siteDataCurrentVersionId";
export * from './validationErrorCtx'; export * from "./siteDataDomain";
export * from './validationErrorLocItem'; export * from "./siteDataPublishedAt";
export * from './verifyCandidatesParams'; export * from "./siteDataTemplateId";
export * from './weatherData'; export * from "./siteDataTheme";
export * from './weatherDataTimezone'; export * from "./siteDataThemeAnyOf";
export * from './weatherDataWindSpeed'; export * from "./siteDataThumbnailUrl";
export * from "./siteStatus";
export * from "./siteVersionData";
export * from "./siteVersionDataBuildError";
export * from "./siteVersionDataBuiltAt";
export * from "./siteVersionDataCreatedAt";
export * from "./sourceType";
export * from "./unitData";
export * from "./userRole";
export * from "./validationError";
export * from "./validationErrorCtx";
export * from "./validationErrorLocItem";
export * from "./verifyCandidatesParams";
export * from "./weatherData";
export * from "./weatherDataTimezone";
export * from "./weatherDataWindSpeed";

View File

@ -4,14 +4,14 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { JobType } from './jobType'; import type { JobType } from "./jobType";
import type { JobStatus } from './jobStatus'; import type { JobStatus } from "./jobStatus";
import type { JobDataResult } from './jobDataResult'; import type { JobDataResult } from "./jobDataResult";
import type { JobDataLastError } from './jobDataLastError'; import type { JobDataLastError } from "./jobDataLastError";
import type { JobDataRunAfter } from './jobDataRunAfter'; import type { JobDataRunAfter } from "./jobDataRunAfter";
import type { JobDataRunStartedAt } from './jobDataRunStartedAt'; import type { JobDataRunStartedAt } from "./jobDataRunStartedAt";
import type { JobDataCreatedAt } from './jobDataCreatedAt'; import type { JobDataCreatedAt } from "./jobDataCreatedAt";
import type { JobDataUpdatedAt } from './jobDataUpdatedAt'; import type { JobDataUpdatedAt } from "./jobDataUpdatedAt";
export interface JobData { export interface JobData {
job_id: string; job_id: string;

View File

@ -11,8 +11,7 @@
전이는 전부 조건부 원자 UPDATE(CAS)로만 한다. 실패는 재시도 가능하면 전이는 전부 조건부 원자 UPDATE(CAS)로만 한다. 실패는 재시도 가능하면
PENDING(run_after=백오프)으로 되돌리고, 소진되면 DEAD(dead-letter). PENDING(run_after=백오프)으로 되돌리고, 소진되면 DEAD(dead-letter).
*/ */
export type JobStatus = typeof JobStatus[keyof typeof JobStatus]; export type JobStatus = (typeof JobStatus)[keyof typeof JobStatus];
// eslint-disable-next-line @typescript-eslint/no-redeclare // eslint-disable-next-line @typescript-eslint/no-redeclare
export const JobStatus = { export const JobStatus = {

View File

@ -11,8 +11,7 @@
무거운 잡(브라우저 필요)과 가벼운 잡(HTTP API 만)을 코드로 갈라 둔다 — 무거운 잡(브라우저 필요)과 가벼운 잡(HTTP API 만)을 코드로 갈라 둔다 —
크롤링 법무 결론이 나면 무거운 잡만 별도 워커 이미지로 분리한다. 크롤링 법무 결론이 나면 무거운 잡만 별도 워커 이미지로 분리한다.
*/ */
export type JobType = typeof JobType[keyof typeof JobType]; export type JobType = (typeof JobType)[keyof typeof JobType];
// eslint-disable-next-line @typescript-eslint/no-redeclare // eslint-disable-next-line @typescript-eslint/no-redeclare
export const JobType = { export const JobType = {
@ -22,4 +21,6 @@ export const JobType = {
BUILD: 4, BUILD: 4,
LOCAL_SYNC: 5, LOCAL_SYNC: 5,
AI_CHECK: 6, AI_CHECK: 6,
SONG: 7,
ROLLBACK: 8,
} as const; } as const;

View File

@ -6,10 +6,9 @@
*/ */
/** /**
* place_links.channel 코드값. Perplexity 가 발견하는 채널 종류. * place_channels.channel 코드값. Perplexity 가 발견하는 채널 종류.
*/ */
export type LinkChannel = typeof LinkChannel[keyof typeof LinkChannel]; export type LinkChannel = (typeof LinkChannel)[keyof typeof LinkChannel];
// eslint-disable-next-line @typescript-eslint/no-redeclare // eslint-disable-next-line @typescript-eslint/no-redeclare
export const LinkChannel = { export const LinkChannel = {

View File

@ -4,11 +4,11 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { LinkChannel } from './linkChannel'; import type { LinkChannel } from "./linkChannel";
import type { LinkDataTitle } from './linkDataTitle'; import type { LinkDataTitle } from "./linkDataTitle";
import type { SourceType } from './sourceType'; import type { SourceType } from "./sourceType";
import type { LinkDataDiscoveredAt } from './linkDataDiscoveredAt'; import type { LinkDataDiscoveredAt } from "./linkDataDiscoveredAt";
import type { LinkDataConfirmedAt } from './linkDataConfirmedAt'; import type { LinkDataConfirmedAt } from "./linkDataConfirmedAt";
export interface LinkData { export interface LinkData {
link_id: string; link_id: string;

View File

@ -4,9 +4,9 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { LocalContentStatus } from './localContentStatus'; import type { LocalContentStatus } from "./localContentStatus";
export type ListContentsParams = { export type ListContentsParams = {
status?: LocalContentStatus | null; status?: LocalContentStatus | null;
region_code?: string | null; region_code?: string | null;
}; };

View File

@ -6,9 +6,9 @@
*/ */
export type ListFactsParams = { export type ListFactsParams = {
/** /**
* 객실·메뉴·프로그램 단위 fact 만 * 객실·메뉴·프로그램 단위 fact 만
*/ */
unit_id?: string | null; unit_id?: string | null;
publishable_only?: boolean; publishable_only?: boolean;
}; };

View File

@ -6,5 +6,5 @@
*/ */
export type ListFaqsParams = { export type ListFaqsParams = {
publishable_only?: boolean; publishable_only?: boolean;
}; };

View File

@ -6,5 +6,5 @@
*/ */
export type ListLinksParams = { export type ListLinksParams = {
confirmed_only?: boolean; confirmed_only?: boolean;
}; };

View File

@ -6,9 +6,9 @@
*/ */
export type ListMediaParams = { export type ListMediaParams = {
/** /**
* 객실·메뉴 단위 사진만 * 객실·메뉴 단위 사진만
*/ */
unit_id?: string | null; unit_id?: string | null;
publishable_only?: boolean; publishable_only?: boolean;
}; };

View File

@ -6,13 +6,13 @@
*/ */
export type ListMySitesParams = { export type ListMySitesParams = {
/** /**
* @minimum 1 * @minimum 1
*/ */
page?: number; page?: number;
/** /**
* @minimum 1 * @minimum 1
* @maximum 100 * @maximum 100
*/ */
size?: number; size?: number;
}; };

View File

@ -4,29 +4,29 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { PlaceCategory } from './placeCategory'; import type { PlaceCategory } from "./placeCategory";
import type { PlaceStatus } from './placeStatus'; import type { PlaceStatus } from "./placeStatus";
export type ListPlacesParams = { export type ListPlacesParams = {
/** /**
* 상호명·주소 부분일치 * 상호명·주소 부분일치
*/ */
search?: string | null; search?: string | null;
/** /**
* 업종 * 업종
*/ */
category?: PlaceCategory | null; category?: PlaceCategory | null;
/** /**
* 상태 * 상태
*/ */
status?: PlaceStatus | null; status?: PlaceStatus | null;
/** /**
* @minimum 1 * @minimum 1
*/ */
page?: number; page?: number;
/** /**
* @minimum 1 * @minimum 1
* @maximum 100 * @maximum 100
*/ */
size?: number; size?: number;
}; };

View File

@ -6,10 +6,10 @@
*/ */
export type ListShowcaseParams = { export type ListShowcaseParams = {
/** /**
* 가져올 개수 * 가져올 개수
* @minimum 1 * @minimum 1
* @maximum 48 * @maximum 48
*/ */
limit?: number; limit?: number;
}; };

View File

@ -4,16 +4,16 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { LocalContentType } from './localContentType'; import type { LocalContentType } from "./localContentType";
import type { LocalSource } from './localSource'; import type { LocalSource } from "./localSource";
import type { LocalContentDataExternalId } from './localContentDataExternalId'; import type { LocalContentDataExternalId } from "./localContentDataExternalId";
import type { LocalContentDataTitle } from './localContentDataTitle'; import type { LocalContentDataTitle } from "./localContentDataTitle";
import type { LocalContentDataBody } from './localContentDataBody'; import type { LocalContentDataBody } from "./localContentDataBody";
import type { LocalContentStatus } from './localContentStatus'; import type { LocalContentStatus } from "./localContentStatus";
import type { LocalContentDataCollectedAt } from './localContentDataCollectedAt'; import type { LocalContentDataCollectedAt } from "./localContentDataCollectedAt";
import type { LocalContentDataPublishedAt } from './localContentDataPublishedAt'; import type { LocalContentDataPublishedAt } from "./localContentDataPublishedAt";
import type { LocalContentDataDisplayStartAt } from './localContentDataDisplayStartAt'; import type { LocalContentDataDisplayStartAt } from "./localContentDataDisplayStartAt";
import type { LocalContentDataDisplayEndAt } from './localContentDataDisplayEndAt'; import type { LocalContentDataDisplayEndAt } from "./localContentDataDisplayEndAt";
export interface LocalContentData { export interface LocalContentData {
local_content_id: string; local_content_id: string;

View File

@ -8,8 +8,8 @@
/** /**
* 운영 관리자가 검수하는 지역 콘텐츠 발행 상태. * 운영 관리자가 검수하는 지역 콘텐츠 발행 상태.
*/ */
export type LocalContentStatus = typeof LocalContentStatus[keyof typeof LocalContentStatus]; export type LocalContentStatus =
(typeof LocalContentStatus)[keyof typeof LocalContentStatus];
// eslint-disable-next-line @typescript-eslint/no-redeclare // eslint-disable-next-line @typescript-eslint/no-redeclare
export const LocalContentStatus = { export const LocalContentStatus = {

View File

@ -8,8 +8,8 @@
/** /**
* local_contents.content_type 코드값. 행정구역 코드 단위로 캐싱되는 지역 정보 종류. * local_contents.content_type 코드값. 행정구역 코드 단위로 캐싱되는 지역 정보 종류.
*/ */
export type LocalContentType = typeof LocalContentType[keyof typeof LocalContentType]; export type LocalContentType =
(typeof LocalContentType)[keyof typeof LocalContentType];
// eslint-disable-next-line @typescript-eslint/no-redeclare // eslint-disable-next-line @typescript-eslint/no-redeclare
export const LocalContentType = { export const LocalContentType = {
@ -17,4 +17,6 @@ export const LocalContentType = {
FESTIVAL: 2, FESTIVAL: 2,
ATTRACTION: 3, ATTRACTION: 3,
RESTAURANT: 4, RESTAURANT: 4,
COURSE: 5,
STORY: 6,
} as const; } as const;

View File

@ -8,8 +8,7 @@
/** /**
* local_contents.source 코드값. 어느 외부 API 에서 왔는지. * local_contents.source 코드값. 어느 외부 API 에서 왔는지.
*/ */
export type LocalSource = typeof LocalSource[keyof typeof LocalSource]; export type LocalSource = (typeof LocalSource)[keyof typeof LocalSource];
// eslint-disable-next-line @typescript-eslint/no-redeclare // eslint-disable-next-line @typescript-eslint/no-redeclare
export const LocalSource = { export const LocalSource = {
@ -17,4 +16,6 @@ export const LocalSource = {
TOUR_API: 2, TOUR_API: 2,
KAKAO_LOCAL: 3, KAKAO_LOCAL: 3,
OFFICIAL_WEB: 4, OFFICIAL_WEB: 4,
LLM: 5,
NAVER_CRAWL: 6,
} as const; } as const;

View File

@ -4,17 +4,17 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { MediaDataUnitId } from './mediaDataUnitId'; import type { MediaDataUnitId } from "./mediaDataUnitId";
import type { MediaDataOriginUrl } from './mediaDataOriginUrl'; import type { MediaDataOriginUrl } from "./mediaDataOriginUrl";
import type { SourceType } from './sourceType'; import type { SourceType } from "./sourceType";
import type { MediaDataSourceUrl } from './mediaDataSourceUrl'; import type { MediaDataSourceUrl } from "./mediaDataSourceUrl";
import type { MediaDataLabel } from './mediaDataLabel'; import type { MediaDataLabel } from "./mediaDataLabel";
import type { MediaDataAltText } from './mediaDataAltText'; import type { MediaDataAltText } from "./mediaDataAltText";
import type { MediaDataVisionConfidence } from './mediaDataVisionConfidence'; import type { MediaDataVisionConfidence } from "./mediaDataVisionConfidence";
import type { MediaStatus } from './mediaStatus'; import type { MediaStatus } from "./mediaStatus";
import type { MediaDataWidth } from './mediaDataWidth'; import type { MediaDataWidth } from "./mediaDataWidth";
import type { MediaDataHeight } from './mediaDataHeight'; import type { MediaDataHeight } from "./mediaDataHeight";
import type { MediaDataCreatedAt } from './mediaDataCreatedAt'; import type { MediaDataCreatedAt } from "./mediaDataCreatedAt";
/** /**
* 사진 1건. * 사진 1건.

View File

@ -8,8 +8,7 @@
/** /**
* media.status 코드값. 비전 결과 신뢰도가 낮으면 자동 반영하지 않고 PENDING_REVIEW 로 둔다. * media.status 코드값. 비전 결과 신뢰도가 낮으면 자동 반영하지 않고 PENDING_REVIEW 로 둔다.
*/ */
export type MediaStatus = typeof MediaStatus[keyof typeof MediaStatus]; export type MediaStatus = (typeof MediaStatus)[keyof typeof MediaStatus];
// eslint-disable-next-line @typescript-eslint/no-redeclare // eslint-disable-next-line @typescript-eslint/no-redeclare
export const MediaStatus = { export const MediaStatus = {

View File

@ -4,16 +4,16 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { PlaceCategory } from './placeCategory'; import type { PlaceCategory } from "./placeCategory";
import type { PlaceStatus } from './placeStatus'; import type { PlaceStatus } from "./placeStatus";
import type { MySiteDataRoadAddress } from './mySiteDataRoadAddress'; import type { MySiteDataRoadAddress } from "./mySiteDataRoadAddress";
import type { MySiteDataCreatedAt } from './mySiteDataCreatedAt'; import type { MySiteDataCreatedAt } from "./mySiteDataCreatedAt";
import type { MySiteDataSiteId } from './mySiteDataSiteId'; import type { MySiteDataSiteId } from "./mySiteDataSiteId";
import type { MySiteDataStatus } from './mySiteDataStatus'; import type { MySiteDataStatus } from "./mySiteDataStatus";
import type { MySiteDataDomain } from './mySiteDataDomain'; import type { MySiteDataDomain } from "./mySiteDataDomain";
import type { MySiteDataTemplateId } from './mySiteDataTemplateId'; import type { MySiteDataTemplateId } from "./mySiteDataTemplateId";
import type { MySiteDataPublishedAt } from './mySiteDataPublishedAt'; import type { MySiteDataPublishedAt } from "./mySiteDataPublishedAt";
import type { MySiteDataThumbnailUrl } from './mySiteDataThumbnailUrl'; import type { MySiteDataThumbnailUrl } from "./mySiteDataThumbnailUrl";
/** /**
* 내 사이트 목록의 한 줄 — 사업장(place) + 사이트(site). * 내 사이트 목록의 한 줄 — 사업장(place) + 사이트(site).
@ -33,7 +33,6 @@ export interface MySiteData {
domain?: MySiteDataDomain; domain?: MySiteDataDomain;
template_id?: MySiteDataTemplateId; template_id?: MySiteDataTemplateId;
published_at?: MySiteDataPublishedAt; published_at?: MySiteDataPublishedAt;
/** 목록 카드의 그림. 발행에 성공해야 채워지고, 발행마다 `?v=` 가 바뀐다(site_thumbnail.public_url). */
thumbnail_url?: MySiteDataThumbnailUrl; thumbnail_url?: MySiteDataThumbnailUrl;
needs_rebuild?: boolean; needs_rebuild?: boolean;
} }

View File

@ -4,6 +4,6 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { SiteStatus } from './siteStatus'; import type { SiteStatus } from "./siteStatus";
export type MySiteDataStatus = SiteStatus | null; export type MySiteDataStatus = SiteStatus | null;

View File

@ -4,15 +4,15 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { PlaceCandidateExternalPlaceId } from './placeCandidateExternalPlaceId'; import type { PlaceCandidateExternalPlaceId } from "./placeCandidateExternalPlaceId";
import type { PlaceCandidateRoadAddress } from './placeCandidateRoadAddress'; import type { PlaceCandidateRoadAddress } from "./placeCandidateRoadAddress";
import type { PlaceCandidateAddress } from './placeCandidateAddress'; import type { PlaceCandidateAddress } from "./placeCandidateAddress";
import type { PlaceCandidatePhone } from './placeCandidatePhone'; import type { PlaceCandidatePhone } from "./placeCandidatePhone";
import type { PlaceCandidateLatitude } from './placeCandidateLatitude'; import type { PlaceCandidateLatitude } from "./placeCandidateLatitude";
import type { PlaceCandidateLongitude } from './placeCandidateLongitude'; import type { PlaceCandidateLongitude } from "./placeCandidateLongitude";
import type { PlaceCandidateCategoryName } from './placeCandidateCategoryName'; import type { PlaceCandidateCategoryName } from "./placeCandidateCategoryName";
import type { PlaceCandidatePlaceUrl } from './placeCandidatePlaceUrl'; import type { PlaceCandidatePlaceUrl } from "./placeCandidatePlaceUrl";
import type { PlaceCandidateNaverPlaceUrl } from './placeCandidateNaverPlaceUrl'; import type { PlaceCandidateNaverPlaceUrl } from "./placeCandidateNaverPlaceUrl";
/** /**
* 외부 장소 DB 에서 찾은 후보 1건. UI 가 이걸 카드로 그려 사람이 고른다. * 외부 장소 DB 에서 찾은 후보 1건. UI 가 이걸 카드로 그려 사람이 고른다.

View File

@ -9,8 +9,7 @@
* places.category 코드값. 업종 — 스키마 파일(common/category_schema/resources/*.json)과 1:1. * places.category 코드값. 업종 — 스키마 파일(common/category_schema/resources/*.json)과 1:1.
업종 추가 = 여기에 코드 추가 + 스키마 파일 1개 추가. 업종 추가 = 여기에 코드 추가 + 스키마 파일 1개 추가.
*/ */
export type PlaceCategory = typeof PlaceCategory[keyof typeof PlaceCategory]; export type PlaceCategory = (typeof PlaceCategory)[keyof typeof PlaceCategory];
// eslint-disable-next-line @typescript-eslint/no-redeclare // eslint-disable-next-line @typescript-eslint/no-redeclare
export const PlaceCategory = { export const PlaceCategory = {

View File

@ -0,0 +1,27 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Web4Ai API
* OpenAPI spec version: 0.1.0
*/
import type { LocalContentType } from "./localContentType";
import type { PlaceContentDataBody } from "./placeContentDataBody";
import type { PlaceContentDataDisplayEndAt } from "./placeContentDataDisplayEndAt";
import type { PlaceContentDataCollectedAt } from "./placeContentDataCollectedAt";
/**
* 업장 반경 주변정보 1건(admin 목록용).
*/
export interface PlaceContentData {
place_content_id: string;
place_id: string;
content_type: LocalContentType;
external_id: string;
title: string;
body: PlaceContentDataBody;
distance_m: number;
has_image: boolean;
hidden: boolean;
display_end_at?: PlaceContentDataDisplayEndAt;
collected_at?: PlaceContentDataCollectedAt;
}

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Web4Ai API
* OpenAPI spec version: 0.1.0
*/
export type PlaceContentDataBody = { [key: string]: unknown };

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Web4Ai API
* OpenAPI spec version: 0.1.0
*/
export type PlaceContentDataCollectedAt = string | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Web4Ai API
* OpenAPI spec version: 0.1.0
*/
export type PlaceContentDataDisplayEndAt = string | null;

View File

@ -4,20 +4,20 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { PlaceCategory } from './placeCategory'; import type { PlaceCategory } from "./placeCategory";
import type { PlaceStatus } from './placeStatus'; import type { PlaceStatus } from "./placeStatus";
import type { PlaceDataOwnerUserId } from './placeDataOwnerUserId'; import type { PlaceDataOwnerUserId } from "./placeDataOwnerUserId";
import type { PlaceDataExternalSource } from './placeDataExternalSource'; import type { PlaceDataExternalSource } from "./placeDataExternalSource";
import type { PlaceDataExternalPlaceId } from './placeDataExternalPlaceId'; import type { PlaceDataExternalPlaceId } from "./placeDataExternalPlaceId";
import type { PlaceDataRoadAddress } from './placeDataRoadAddress'; import type { PlaceDataRoadAddress } from "./placeDataRoadAddress";
import type { PlaceDataAddress } from './placeDataAddress'; import type { PlaceDataAddress } from "./placeDataAddress";
import type { PlaceDataPhone } from './placeDataPhone'; import type { PlaceDataPhone } from "./placeDataPhone";
import type { PlaceDataLatitude } from './placeDataLatitude'; import type { PlaceDataLatitude } from "./placeDataLatitude";
import type { PlaceDataLongitude } from './placeDataLongitude'; import type { PlaceDataLongitude } from "./placeDataLongitude";
import type { PlaceDataRegionCode } from './placeDataRegionCode'; import type { PlaceDataRegionCode } from "./placeDataRegionCode";
import type { PlaceDataVerifiedAt } from './placeDataVerifiedAt'; import type { PlaceDataVerifiedAt } from "./placeDataVerifiedAt";
import type { PlaceDataContentUpdatedAt } from './placeDataContentUpdatedAt'; import type { PlaceDataContentUpdatedAt } from "./placeDataContentUpdatedAt";
import type { PlaceDataCreatedAt } from './placeDataCreatedAt'; import type { PlaceDataCreatedAt } from "./placeDataCreatedAt";
export interface PlaceData { export interface PlaceData {
place_id: string; place_id: string;

View File

@ -4,6 +4,6 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { ExternalPlaceSource } from './externalPlaceSource'; import type { ExternalPlaceSource } from "./externalPlaceSource";
export type PlaceDataExternalSource = ExternalPlaceSource | null; export type PlaceDataExternalSource = ExternalPlaceSource | null;

View File

@ -4,9 +4,10 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { PlaceSearchItemRoadAddress } from './placeSearchItemRoadAddress'; import type { PlaceSearchItemRoadAddress } from "./placeSearchItemRoadAddress";
import type { PlaceSearchItemCategoryName } from './placeSearchItemCategoryName'; import type { PlaceSearchItemCategoryName } from "./placeSearchItemCategoryName";
import type { PlaceSearchItemCategory } from './placeSearchItemCategory'; import type { PlaceSearchItemCategory } from "./placeSearchItemCategory";
import type { PlaceSearchItemNaverPlaceUrl } from "./placeSearchItemNaverPlaceUrl";
/** /**
* 공개 검색 결과 1건. * 공개 검색 결과 1건.
@ -21,6 +22,5 @@ export interface PlaceSearchItem {
road_address?: PlaceSearchItemRoadAddress; road_address?: PlaceSearchItemRoadAddress;
category_name?: PlaceSearchItemCategoryName; category_name?: PlaceSearchItemCategoryName;
category?: PlaceSearchItemCategory; category?: PlaceSearchItemCategory;
/** @nullable */ naver_place_url?: PlaceSearchItemNaverPlaceUrl;
naver_place_url?: string | null;
} }

View File

@ -4,6 +4,6 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { PlaceCategory } from './placeCategory'; import type { PlaceCategory } from "./placeCategory";
export type PlaceSearchItemCategory = PlaceCategory | null; export type PlaceSearchItemCategory = PlaceCategory | null;

View File

@ -9,8 +9,7 @@
* places.status 코드값. 사업장 생애주기. * places.status 코드값. 사업장 생애주기.
해지는 삭제가 아니라 SUSPENDED 로의 상태 전이다(색인된 페이지를 갑자기 404 로 만들지 않는다). 해지는 삭제가 아니라 SUSPENDED 로의 상태 전이다(색인된 페이지를 갑자기 404 로 만들지 않는다).
*/ */
export type PlaceStatus = typeof PlaceStatus[keyof typeof PlaceStatus]; export type PlaceStatus = (typeof PlaceStatus)[keyof typeof PlaceStatus];
// eslint-disable-next-line @typescript-eslint/no-redeclare // eslint-disable-next-line @typescript-eslint/no-redeclare
export const PlaceStatus = { export const PlaceStatus = {

View File

@ -8,8 +8,7 @@
/** /**
* publish_logs.action 코드값. * publish_logs.action 코드값.
*/ */
export type PublishAction = typeof PublishAction[keyof typeof PublishAction]; export type PublishAction = (typeof PublishAction)[keyof typeof PublishAction];
// eslint-disable-next-line @typescript-eslint/no-redeclare // eslint-disable-next-line @typescript-eslint/no-redeclare
export const PublishAction = { export const PublishAction = {
@ -18,4 +17,5 @@ export const PublishAction = {
REBUILD: 3, REBUILD: 3,
SUSPEND: 4, SUSPEND: 4,
RESUME: 5, RESUME: 5,
ROLLBACK: 6,
} as const; } as const;

View File

@ -4,11 +4,11 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { PublishAction } from './publishAction'; import type { PublishAction } from "./publishAction";
import type { PublishResult } from './publishResult'; import type { PublishResult } from "./publishResult";
import type { PublishLogDataRejectReason } from './publishLogDataRejectReason'; import type { PublishLogDataRejectReason } from "./publishLogDataRejectReason";
import type { PublishLogDataDetail } from './publishLogDataDetail'; import type { PublishLogDataDetail } from "./publishLogDataDetail";
import type { PublishLogDataCreatedAt } from './publishLogDataCreatedAt'; import type { PublishLogDataCreatedAt } from "./publishLogDataCreatedAt";
export interface PublishLogData { export interface PublishLogData {
publish_log_id: string; publish_log_id: string;

View File

@ -4,6 +4,6 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { PublishRejectReason } from './publishRejectReason'; import type { PublishRejectReason } from "./publishRejectReason";
export type PublishLogDataRejectReason = PublishRejectReason | null; export type PublishLogDataRejectReason = PublishRejectReason | null;

View File

@ -8,8 +8,8 @@
/** /**
* publish_logs.reject_reason 코드값. 검수 게이트가 발행을 막은 이유(절대규칙 1~3). * publish_logs.reject_reason 코드값. 검수 게이트가 발행을 막은 이유(절대규칙 1~3).
*/ */
export type PublishRejectReason = typeof PublishRejectReason[keyof typeof PublishRejectReason]; export type PublishRejectReason =
(typeof PublishRejectReason)[keyof typeof PublishRejectReason];
// eslint-disable-next-line @typescript-eslint/no-redeclare // eslint-disable-next-line @typescript-eslint/no-redeclare
export const PublishRejectReason = { export const PublishRejectReason = {

View File

@ -8,8 +8,7 @@
/** /**
* publish_logs.result 코드값. * publish_logs.result 코드값.
*/ */
export type PublishResult = typeof PublishResult[keyof typeof PublishResult]; export type PublishResult = (typeof PublishResult)[keyof typeof PublishResult];
// eslint-disable-next-line @typescript-eslint/no-redeclare // eslint-disable-next-line @typescript-eslint/no-redeclare
export const PublishResult = { export const PublishResult = {

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Web4Ai API
* OpenAPI spec version: 0.1.0
*/
export type RelayParams = {
/**
* @minLength 8
* @maxLength 2000
*/
url: string;
};

View File

@ -4,9 +4,9 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { RenderStatusDataRenderedAt } from './renderStatusDataRenderedAt'; import type { RenderStatusDataRenderedAt } from "./renderStatusDataRenderedAt";
import type { RenderStatusDataRenderedVersion } from './renderStatusDataRenderedVersion'; import type { RenderStatusDataRenderedVersion } from "./renderStatusDataRenderedVersion";
import type { RenderStatusDataError } from './renderStatusDataError'; import type { RenderStatusDataError } from "./renderStatusDataError";
/** /**
* 정적 페이지가 실제로 구워졌는지. 프리렌더가 남긴 보고서를 그대로 옮긴다. * 정적 페이지가 실제로 구워졌는지. 프리렌더가 남긴 보고서를 그대로 옮긴다.

View File

@ -4,7 +4,7 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { ReqCreateFaqSortOrder } from './reqCreateFaqSortOrder'; import type { ReqCreateFaqSortOrder } from "./reqCreateFaqSortOrder";
/** /**
* 사장님이 직접 쓴 FAQ. * 사장님이 직접 쓴 FAQ.

View File

@ -4,9 +4,9 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { LinkChannel } from './linkChannel'; import type { LinkChannel } from "./linkChannel";
import type { ReqCreateLinkTitle } from './reqCreateLinkTitle'; import type { ReqCreateLinkTitle } from "./reqCreateLinkTitle";
import type { SourceType } from './sourceType'; import type { SourceType } from "./sourceType";
export interface ReqCreateLink { export interface ReqCreateLink {
channel?: LinkChannel; channel?: LinkChannel;

View File

@ -4,11 +4,9 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { PlaceCategory } from './placeCategory'; import type { PlaceCategory } from "./placeCategory";
import type { ReqCreatePlaceOwnerUserId } from './reqCreatePlaceOwnerUserId';
export interface ReqCreatePlace { export interface ReqCreatePlace {
name?: string; name?: string;
category?: PlaceCategory; category?: PlaceCategory;
owner_user_id?: ReqCreatePlaceOwnerUserId;
} }

View File

@ -0,0 +1,10 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Web4Ai API
* OpenAPI spec version: 0.1.0
*/
export interface ReqHidePlaceContent {
hidden: boolean;
}

View File

@ -0,0 +1,18 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Web4Ai API
* OpenAPI spec version: 0.1.0
*/
/**
* 예전 버전으로 공개 주소를 되돌린다.
★ 재굽기가 아니다 — 대상 버전이 디스크에 아직 있으면 심볼릭 링크만 돌린다. 지워졌으면
(보관 정책, prerender.ts pruneOldVersions) site_versions.snapshot 으로 다시 굽고 나서
돌린다. 어느 경우든 게이트를 다시 통과해야 한다(사장님이 이미 확인한 값이라 대개는
그대로 통과한다).
*/
export interface ReqRollback {
target_version: number;
}

View File

@ -4,7 +4,7 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { ReqSignupName } from './reqSignupName'; import type { ReqSignupName } from "./reqSignupName";
/** /**
* id/pw 가입. 가입 = 계정 1개다. * id/pw 가입. 가입 = 계정 1개다.

View File

@ -4,7 +4,7 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { PublishAction } from './publishAction'; import type { PublishAction } from "./publishAction";
/** /**
* 발행 상태 전이. ★ 해지는 삭제가 아니라 상태 전이다 — * 발행 상태 전이. ★ 해지는 삭제가 아니라 상태 전이다 —

View File

@ -4,7 +4,7 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { ReqSiteThemeTheme } from './reqSiteThemeTheme'; import type { ReqSiteThemeTheme } from "./reqSiteThemeTheme";
/** /**
* 디자인(색·서체·섹션) 저장. 에디터 좌측 패널과 [디자인] 탭이 만든 결과 그대로 온다. * 디자인(색·서체·섹션) 저장. 에디터 좌측 패널과 [디자인] 탭이 만든 결과 그대로 온다.

View File

@ -10,4 +10,6 @@
★ 확인된 fact 만 근거로 쓴다. 근거가 없으면 생성하지 않는다(유료 호출조차 안 한다). ★ 확인된 fact 만 근거로 쓴다. 근거가 없으면 생성하지 않는다(유료 호출조차 안 한다).
*/ */
export interface ReqStartCopy { [key: string]: unknown } export interface ReqStartCopy {
[key: string]: unknown;
}

View File

@ -1,18 +0,0 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Web4Ai API
* OpenAPI spec version: 0.1.0
*/
import type { ReqSyncFestivalsAreaCode } from './reqSyncFestivalsAreaCode';
import type { ReqSyncFestivalsStartDate } from './reqSyncFestivalsStartDate';
export interface ReqSyncFestivals {
/**
* @minLength 1
* @maxLength 10
*/
region_code: string;
area_code?: ReqSyncFestivalsAreaCode;
start_date?: ReqSyncFestivalsStartDate;
}

View File

@ -4,8 +4,8 @@
* Web4Ai API * Web4Ai API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { FactStatus } from './factStatus'; import type { FactStatus } from "./factStatus";
import type { ReqTransitionFactValue } from './reqTransitionFactValue'; import type { ReqTransitionFactValue } from "./reqTransitionFactValue";
/** /**
* 검증 상태 전이. 허용 전이는 FACT_STATUS_TRANSITIONS 가 유일한 소스다. * 검증 상태 전이. 허용 전이는 FACT_STATUS_TRANSITIONS 가 유일한 소스다.

Some files were not shown because too many files have changed in this diff Show More