Synaplan
开源 AI 软件平台,面向企业:自托管 AI 代理,符合 GDPR 的聊天机器人小部件,RAG 知识库,WhatsApp 与 Outlook AI,MCP,多模型路由。支持 Docker、Kubernetes 或在 Germ 托管。
AI 翻译
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
기업 해외연수(MICE) 견적을 웹에서 즉시 산출해 고객에게 제공하는 서비스. 공개 사이트: 이 파일은 입구 안내다. 작업 규칙은 CLAUDE.md, 지금 어디까지 했는지는 git log --oneline에 있다(커밋 메시지가 곧 작업 기록이다). 역할이 겹치지 않게 유지할 것 — 상태를 문서에 따로 적기 시작하면 코드와 어긋난다. 정적 프론트 + Vercel 서버리스 함수 + Neon Postgres. 빌드 단계가 없다 — 루트가 그대로 정적 출력이라 커밋한 파일이 공개 도메인에서 읽힌다 (그래서 내부 문서·검증 도구는 .vercelignore로 업로드 자체를 막는다).查看完整 README ↗
기업 해외연수(MICE) 견적을 웹에서 즉시 산출해 고객에게 제공하는 서비스. 공개 사이트: https://bizpage-estimate.vercel.app
이 파일은 입구 안내다. 작업 규칙은 CLAUDE.md, 지금 어디까지 했는지는
git log --oneline에 있다(커밋 메시지가 곧 작업 기록이다). 역할이 겹치지 않게 유지할 것 —
상태를 문서에 따로 적기 시작하면 코드와 어긋난다.
정적 프론트 + Vercel 서버리스 함수 + Neon Postgres. 빌드 단계가 없다 —
루트가 그대로 정적 출력이라 커밋한 파일이 공개 도메인에서 읽힌다
(그래서 내부 문서·검증 도구는 .vercelignore로 업로드 자체를 막는다).
| 파일 | 역할 |
|---|---|
index.html · styles.css | 고객이 보는 랜딩·견적 계산기 화면 |
script.js | 견적 엔진(요율·계수·시즌·환율 계산) + 화면 로직 |
data.js | 55개 목적지 요율표 + DEST_CLASSIFY 분류표(좌석·보험·지역·통화·시즌·반구) + 추천 콘텐츠(DEST_REC)·추천 일정(ITINERARY_DB) + 성수기 달력(PEAK_CALENDAR·LUNAR_PEAKS — 2026-10-02 script.js에서 옮김, 서버가 공휴일과 대조한다) |
dest_currency.js · company-info.js | 정산 통화 매핑 · 회사 정보 |
admin.html | 관리자 화면 — 요율 편집, 문의·견적 관리, 통계, 일정 관리, 계정 관리 |
admin-quote-pro.html | 내부직원용 견적 산출 — 같은 엔진·같은 금액에 원가·마진이 함께 보이고 항목별 조정·일정 입력·견적서 미리보기까지 한 화면 |
estimate-view.html | 고객에게 공유되는 견적서 화면 |
quote_engine_host.js | 견적 엔진 호스트 — 엔진(script.js)이 읽는 입력칸을 코드로 만든다. 화면을 베껴 세 번째 복사본을 만들지 않으려고 생겼다. mount()는 script.js보다 먼저, ready({…})는 뒤에 부른다 |
quote_doc.js · quote_doc.css | 견적서 공통 모듈 — 견적서·일정표를 그리는 유일한 자리. 규격(blank)·변환·렌더가 여기 한 곳이다. _로 시작하는 키 = 내부 전용(stripInternal이 지운다) |
api/ | 서버리스 함수(인증·요율·문의·견적·공유링크·통계) |
api/_lib/external_feeds.js | 바깥 공식 자료 매일 받기 — 환율(수출입은행 우선 + 보조 출처)·한국 공휴일(특일정보) + 공휴일 ↔ 성수기 달력 대조. 매일 05:00 api/rates.js?cron=1이 부르고 결과를 app_settings.ext_feed_status에 남긴다(요율 관리 「🔄 외부 자료 자동 갱신」). 인증키는 Vercel 환경변수 EXIM_API_KEY·DATA_GO_KR_KEY — 없으면 「키 없음」이라고 말한다. 금액을 스스로 바꾸는 것은 환율 표뿐 |
api/_lib/fuel_surcharge.js | ⛽ 유류할증료 월별 표 — 항공사가 자동 수집을 막아(대한항공 403·아시아나 목록 차단, 우회 금지) 기준 = 대한항공(12/17 합병). 담당자가 매달 대한항공 공지 본문을 붙여넣거나(서버가 받으러 가지 않는다) 합병 전까지는 아시아나 공지 링크를 넣으면 서버가 읽고 검산(9구간·이어짐·거리순 금액·적용 월)해야 app_settings.fuel_tables에 저장한다. 목적지 → 구간은 data.js DEST_FUEL_ROUTE(대표 공항·대권거리·공지 도시 표기). 요율 관리 「⛽ 유류할증료 월별 표」. ⚠ 아직 견적 금액에 안 쓰인다(결정대기열 0-ap) |
ai-loop/ | 검증·감사 도구와 테스트 (런타임 아님, 배포 제외) |
이미지/ | 목적지 사진·세계지도 |
요율의 진실은 data.js가 아니라 운영 DB(rate_overrides)다. data.js는 폴백
기본값이라 점점 낡는다. 실제 값은 curl -s .../api/rates로 확인한다.
추천 일정·추천 콘텐츠도 같은 구조다 — data.js의 ITINERARY_DB(코스)와
DEST_REC(방식 A/B)가 기본값이고, 관리자 → 일정 관리에서 저장한 값이
itinerary_overrides에 얹혀 고객에게 나간다
(curl -s '.../api/content?action=itineraries').
node ai-loop/run_all_tests.js # 회귀 스위트 — 계산식을 바꿨으면 무조건 이것부터
node ai-loop/fuzz_invariants.js # 견적 엔진 불변식 (--full = 52,360건)
node ai-loop/audit_consistency.js # 목적지 목록 교차 정합성 (오류 0이어야 정상)
node ai-loop/audit_rates.js # 요율 '값' 점검 — 결과는 '확인 대상'이지 '오류'가 아니다
node ai-loop/audit_season_match.js # 적어 둔 현지 시즌 vs 금액에 쓰는 시즌표 — 뒤집힌 곳을 센다
node ai-loop/audit_calendar_stack.js # 월 시즌 × 날짜 피크가 겹쳐 몇 배까지 붙는지 — 시즌표를 고칠 때 함께 볼 것
node ai-loop/audit_hotel_grade.js # 등급을 올렸는데 총액이 내려가는 자리 — 마진 밴드 계단 (대기열 0-af)
node ai-loop/audit_amount_drift.js # 🔴 **옛 커밋의 코드로 직접 돌려** 고객 금액이 움직였는지 대조 — 스냅샷을 못 믿을 때
node ai-loop/db_migrate_adhoc.js # 직접견적 → quotes 이관 (기본 dry-run · `--apply`는 스냅샷 먼저)
node ai-loop/audit_internals.js # 안쪽 세기 (XQ) — 아무도 안 부르는 파일 · 함수 수 vs 12개 한도 ·
# 두 번 적힌 목록 · 큰 파일. **오류가 아니라 사람이 볼 목록**이다
node ai-loop/probe_admin_size.js # admin.html 안에 무엇이 들어 있나 (XR) — --all로 전부
node ai-loop/audit_ux.js # 🔴 **화면을 보자마자 이해되는가** (XT) — 전 화면 7개
node ai-loop/audit_ux.js --verbose # 걸린 것을 전부 나열
운영 쪽(DB·백업)은 따로 본다 — 코드가 멀쩡해도 여기서 조용히 무너진다:
node ai-loop/audit_backup_health.js # 🔴 자동 백업이 조용히 멈췄는가 (YO) — 「백업」 절 참조
node ai-loop/audit_deletions.js # 🔴 무엇이 지워졌는가 (YP) — 표·행 전체·누가·왜
node ai-loop/audit_deletions.js --id 42 # 그 건의 행을 펼친다(되돌릴 근거)
node ai-loop/audit_conclusions.js # 🔴 **서 있는 결론이 아직 유효한가** (YU)
🔴 측정으로 내린 결론은 그때 표본에서 내린 것이다 — 그런데 견적서는 계속 는다.
「요율 천장」(VL)·「골프 축은 우연」(YF)·「마진이 얇지 않다」(YI) 같은 결론에
근거 표본 수가 붙어 있지 않으면, 다음 사람은 낡은 결론을 사실로 들고 간다.
(메모리에 「28/36만 옮겨 적지 말 것」 같은 경고가 여러 번 있는 것이 그 증거다.)
audit_conclusions.js가 지금 표본과 대조해 다시 재야 할 것만 말한다.
⚠ 표본 종류를 가른다 — 전체 문서가 늘어도 역검증 표본은 안 늘 수 있다.
⚠ 문턱은 20%(잘 뒤집히는 결론은 10%). 막지 않고 말만 한다.
⚠ 새 결론을 내렸으면 그 파일의 결론들에 한 줄 넣을 것 — 안 넣으면 그 결론도
조용히 낡는다. 근거는 커밋에 두고 여기엔 「몇 건에서 내렸나」만 적는다.
🔴 삭제는 api/_lib/deletion_log.js 한 곳을 지난다 (YP). 삭제 자리가 6개 파일에
9곳이라 각자 기록하게 두면 반드시 하나를 빠뜨린다(결함 생성기 ①).
api/ 어디에도 직접 delete from을 쓰지 말 것 — test_yP_deletion_log.js가
파일을 훑어 막는다. 새 표를 지우게 되면 그 모듈의 KNOWN_TABLES에 먼저 적는다.
⚠ 순서가 핵심이다: 읽기 → 기록 → 삭제. 기록이 실패하면 삭제가 아예 안 간다
(뒤집으면 기록 실패 시 아무 데도 안 남는 삭제가 생긴다).
⚠ deletion_log 스냅샷에는 고객 연락처가 들어 있다. 읽는 길을 낼 때는 반드시
requireAdmin 뒤에 둔다 — 지금은 어떤 API도 이 표를 읽지 않고, 그 사실을 테스트가 지킨다.
🔴 audit_ux.js가 재는 것 (대표 지시 2026-08-27 — 「고객이든 관리자든 화면을 봤을 때
바로바로 이해되는 구조」). 셀 수 있는 것만 센다 — 색·여백·글꼴은 안 다룬다.
잴 수 없는 것을 재는 척하면 그 숫자가 오히려 판단을 막는다.
| 재는 것 | 왜 |
|---|---|
| 이 화면이 무엇인지 말하는가(제목) | 탭을 눌렀는데 여기가 어딘지 모르면 그 자리에서 멈춘다 |
| 주요 행동이 하나로 도드라지는가 | 강조 버튼이 둘 이상이면 무엇부터 눌러야 할지 모른다 |
| 버튼이 무슨 일이 날지 말하는가 | 「확인」·「적용」은 무슨 일이 나는지 안 말한다 |
| 🔴 영문·기술 용어가 눈에 보이는가 | XS에서 「golfRounds」가 고객 화면에 그대로 나갔다 |
| 비어 있을 때 다음 행동을 말하는가 | 「데이터가 없습니다」로 끝나면 첫날 담당자는 아무것도 못 한다 |
| 낭독기가 읽을 이름이 없는 것 | 아이콘만 있는 버튼·라벨 없는 칸 |
| 한 번에 보이는 조작 수 | 인지 부하 |
⚠ 관리자 화면은 로그인부터 통과해야 아무것도 안 보인다(_admin_fixtures.js가 그
답을 준다). 안 그러면 로그인 폼만 재고 「깨끗하다」고 말하게 된다(결함 생성기 ③).
⚠ 결과는 전부 확인 대상이다. 오류가 아니다 — audit_rates와 같은 원칙.
🔴 admin.html(865KB·13,881줄)을 쪼개려 할 때 먼저 읽을 것. 2026-08-27 실측:
동작 76% · 뼈대 16% · 모양 9%이고, 인라인 <script> 하나가 10,813줄이다.
그런데 그 안은 최상위 조각 493개로 이미 잘게 갈려 있다(가장 큰 것이 365줄, 3%) —
떼어낼 괴물 함수가 없다. 경계가 있다면 이름 첫 낱말이다(render 17% · iti 13% ·
rec 9% · pkg 8%).
🔴 그리고 지금은 쪼개면 안 된다. ai-loop의 검사 74개가 admin.html을 글자로
읽어 정규식으로 대조한다. 인라인 스크립트를 바깥 파일로 빼면 그 74개는 읽을 글자를
잃고 조용히 통과한다(터지지 않는다 — 그래서 더 위험하다. XQ에서 목록을 단일 출처로
모았을 때 정확히 이 일이 다섯 건 났다). 순서는 검사가 파일을 따라오게 만드는 것이
먼저다 — 74개가 「admin의 스크립트」를 한 곳에서 받아 가게 바꾼 뒤에 쪼갠다.
🔴 견적서를 폴더에 일괄로 넣었으면 이것부터 (YT) — 「이번에 봐야 할 것」만 한 장으로:
node ai-loop/report_intake.js # 새로 들어온 것 중 봐야 할 것 (캐시 사용)
node ai-loop/report_intake.js --all # 새 것만이 아니라 전부
node ai-loop/report_intake.js --mark # 다 봤다고 표시(다음엔 새 것만 나온다)
줄 세우는 기준은 대표 방침 그대로 「틀린 값」이 「빈칸」보다 위다 — 🔴는 요율에 틀린 값이 얹힐 수 있는 것(덜 읽음·두 번 셈·여러 도시·목적지 미정), 🟡는 표본에 못 들어갈 뿐 금액은 안 틀리는 것(정답지 없음·환율 없음·출발일 없음). ⚠ 판정을 새로 하지 않는다 — 커버리지(YS)·다도시(TF)·골프(YD)의 기존 판정을 모아 세울 뿐이다. 여기에 문턱을 다시 적으면 두 벌이 된다.
견적서 PDF 추출을 손댔으면 코퍼스로 재 본다 (읽기 전용, 코퍼스는 저장소 밖).
node ai-loop/audit_self_checks.js # **추출기가 스스로 돌리는 검산 두 개** — 정답지가 맞는가 (WD)
node ai-loop/audit_vacuous_rows.js # 검산 안 된 줄이 대표 단가가 되는가 — 신뢰도까지 본다
node ai-loop/audit_row_categories.js # 검산줄이 어느 항목인지 알아냈는가 — 못 알아낸 이유까지 센다
node ai-loop/audit_coverage.js # 우리가 읽은 줄이 총계의 몇 %를 설명하는가 — 덜 읽은 것을 잡는다
node ai-loop/audit_extract_sanity.js # 뽑아낸 값이 말이 되는가 — 동료 대비(🔴) · 요율표 대비(🟡) ·
# **전 일정 총액이 1일 단가 자리에 왔는가(📏, SV)**
node ai-loop/backtest_quotes.js # 고객이 보는 금액의 오차 (--cache = 추출 결과 재사용)
node ai-loop/audit_error_decomp.js # **그 오차가 어느 칸에서 왔는가** + 🎯요율 천장 (VL)
node ai-loop/audit_spec_knobs.js # 엔진이 가진 **사양 손잡이**로 담을 수 있는가 (VM)
node ai-loop/audit_corpus_fitness.js # **이 견적서가 우리와 같은 상품인가** — 정답지의 성격 (VN)
node ai-loop/audit_itinerary.js # 일정표를 얼마나 읽어냈는가 (트랙 B — 금액과 무관)
🔴 실측을 요율에 올리는 자리 — apply_rate_updates.js. 기본이 dry-run이고,
--apply를 명시해야 운영 DB에 쓴다. 고객이 보는 금액이 바뀌는 유일한 자동 경로라
가드가 겹쳐 있다. 그중 둘은 「견적서가 계속 쌓인다」는 전제로 만든 것이다 (YR):
golf_fee 칸은 면제다(골프 요금이 골프 문서에서 나오는 건 당연하다).
· 골프·비골프가 섞여 있으면 보류하지 않는다 — 견줄 근거가 있다는 뜻이다.
· 🔴 골프 판정을 못 하면 --apply가 멈춘다. 모르는 것을 「골프 아님」으로
넘기면 가드가 있으나 마나다. 정말 없이 진행하려면 --skip-golf-guard를 적는다.
· ⛳로 걸린 것은 보류 목록 맨 위에 전부 나온다(30개 자르기에 안 묻힌다).한 장을 파고들 때 · 이미 DB에 들어간 값을 되짚을 때 (전부 읽기 전용).
node ai-loop/probe_corpus_doc.js "신한 …(발리).pdf" # 문서 한 장을 사람 눈으로 보기 전에 훑는다 (XA)
node ai-loop/probe_corpus_doc.js --all-missing # 검산이 안 돈 문서만 — **먼저 「문서에 값이 있는가」**
node ai-loop/recheck_reports.js # 운영 DB 제보를 원본 견적서로 다시 맞춰 본다 (TX)
node ai-loop/resolve_far_off.js # 「확인 필요」가 오독인가 · 요율이 낡은 것인가를 가른다 (TY)
한 번 쓰고 끝난 것 — 지우지 않은 이유는 무엇을 왜 그렇게 했는지가 그 안에 있어서다.
새로 만들 것을 찾을 때 여기부터 보지 말 것:
fix_report_marks.js(TI 이전 제보에 자동 제외 표시를 소급 — 기본 dry-run, 쓰려면 --apply) ·
probe_p11_interactions.js(계수 곱셈 스택을 고치기 전에 재 둔 측정).
정확도 도구 셋은 층이 다르다 — 섞어 읽으면 엉뚱한 칸을 고치게 된다:
| 도구 | 답하는 것 | 답하지 못하는 것 |
|---|---|---|
audit_self_checks | 정답지 자체가 맞는가(문서가 스스로 검산된다) | 엔진이 맞는가 |
backtest_quotes | 한 건이 몇 % 어긋나는가 | 왜 어긋나는가 |
audit_rate_calibration | 목적지 × 칸이 실측과 몇 배 벌어졌나 | 그 배수가 총액을 얼마나 움직이는가 |
audit_error_decomp | 한 건의 오차를 칸별로 쪼갠 것 · 요율로 갈 수 있는 천장 | 요율 밖 원인(좌석 등급·섭외비)의 정체 |
audit_staff_overrides | 담당자가 엔진 금액을 어디서 얼마나 고치나(운영 백업을 읽는다, zS) · 확정가 대비 | PDF 코퍼스 — 실사용 견적만 본다. 대표 계정 수정은 근거로 안 센다 |
🔴 2026-08-21에 가설 둘이 실측으로 기각됐다. 다시 세우지 말 것:
「요율을 더 다듬으면 정확해진다」(VL) — 견적서마다 요율을 완벽히 맞춰도 폭이 22.6% → 27.7%로 나빠진다(목표 안 15 → 14건). 원가 기준으로 재도 같다.
「사양(좌석·객실)을 물어보면 정확해진다」(VM) — 손잡이를 자유롭게 돌리면 목표 안이 15 → 28건까지 가지만 문서가 뒷받침하는 fit은 0/11건이다. 코퍼스 45건에 「비즈니스석」 낱말이 0건인데 탐색은 7건에 비즈니스를 골랐다. 문서가 말하는 「1인1실」을 그대로 넣으면 오히려 7 → 3건으로 나빠진다 (그 낱말은 전원 사양이 아니라 한 줄 표기이거나 조건문이다).
「표본에 성격이 다른 문서가 섞여 폭이 넓다」(VN) — 축 다섯을 전부 통과한 6건은 폭이 22.6% → 12.0%로 좁다. 그런데 같은 크기 무작위 6건 2,000번과 견주니 하위 13% — 우연 범위와 겹친다. 「6건이라 좁은 것」과 구분되지 않는다. 가장 유력한 축(ⓒ 알선 수수료 줄)도 하위 12%로 경계선이다. 표본이 더 쌓여야 한다.
⚠ audit_spec_knobs의 「28/36」, audit_corpus_fitness의 「폭 12.0%」만 옮겨 적지 말 것 —
둘 다 상한이거나 착시일 수 있다. 두 도구가 그 사실을 스스로 찍는다.
✅ 대신 확실해진 것: 골프 축은 폭이 아니라 중앙값으로 드러난다.
🔴 2026-09-02(YD)에 이 축을 처음 제대로 쟀다. 그전에는 골프 판정이 「골프」라는
낱말만 세어 「BRG CC 18홀 라운딩」으로 적은 문서를 놓쳤고, 그래서 양쪽 무리가 3건씩이라
MIN_GROUP(4)에 못 미쳐 「아직 못 잰다」로만 출력되고 있었다. 지금 실측:
① 골프인데 요율에 골프 요금 **없음** 4건 중앙값 **-17.7%** (다낭 2 · 푸꾸옥 · 발리)
② 골프이고 요율에 골프 요금 있음 4건 중앙값 -1.1%
③ 골프 일정 아님 28건 중앙값 +2.7%
→ **10개 축 중 2위(+20.4%)**이고, 원인이 설명되는 몇 안 되는 축이다(엔진이 그 줄을 아예 못 만든다). 처방은 요율 조정이 아니라 골프 요금 4곳을 넣는 것 — 대기열 0-m.
→ 남은 폭은 엔진이 담을 칸이 아예 없는 돈 쪽에 있다(audit_gap_source의 미분류 12.4%).
⚠ 네 도구는 같은 표본을 봐야 뜻이 통한다. 그래서 판정(_comparable.js) ·
추출 캐시(_corpus_cache.js) · 목표선(_accuracy_target.js) · 엔진 상수
(_engine_consts.js)를 단일 출처로 뺐다. 도구를 새로 만들 때 이 넷을 다시 적지 말 것 —
test_vL_error_decomp.js와 test_vI_target_band.js가 리터럴 사본을 금지한다.
⚠ 문서를 성격으로 가르는 판정도 단일 출처다(YD·YE). 둘 다 원래는 쓰는 곳에 저마다 정규식을 두고 있었고, 둘 다 틀린 채로 돌고 있었다:
_golf_scope.js — 골프 일정인가. 갈래 수로 센다(「골프」 낱말만 세면 놓친다)._issuer.js — 어느 회사가 발행했나. 문서의 「발신」 칸만 근거로 쓰고 나머지는 미상.
본문 아무 데나 이름이 있으면 발행사로 찍던 때는 45건 중 **34건이 거짓 「하나투어」**였다
(근거가 대부분 「"하나투어" 테이블에서 미팅」이라는 일정표 줄이었다).⚠ 일정표(L7)는 금액과 완전히 분리된 층이다. audit_itinerary.js가 좋아져도 고객이
보는 금액은 1원도 안 바뀐다. 거꾸로 단가 쪽을 고칠 때 이 자를 근거로 삼지 말 것.
--show "북해도"로 그 문서에서 읽은 일정을 눈으로 확인할 수 있다.
보이는 모양을 손댔으면 하나 더 — 실제 브라우저로 띄워 좌표를 잰다.
python ai-loop/check_manual_layout.py # 매뉴얼 줄맞춤 (데스크톱·태블릿·모바일)
python ai-loop/check_editor_layout.py # 일정 편집 화면 (5폭 + 화면 세로 길이)
python ai-loop/check_quote_doc_layout.py # 고객 견적서 — 고객이 하는 그대로 뽑아 잰다
python ai-loop/check_manual_layout.py --shots # 스크린샷도 저장 (--shots는 셋 다 있다)
python ai-loop/check_contrast.py # 안 읽히는 글자 — 31개 화면(고객 견적서·패키지 포함)
python ai-loop/check_contrast.py --all # 확인 대상(흐린 글자)까지 전부
python ai-loop/check_quote_form_layout.py # 🔴 고객 견적 폼 — 폼에 도착했을 때 무엇이 보이나 (XT)
python ai-loop/check_customer_screens.py # 🔴 고객이 손에 쥐는 화면 — 폰 폭부터 (YA)
python ai-loop/check_customer_screens.py --selftest # 안전망이 살아 있는지 (고장 주입)
python ai-loop/check_admin_screens.py # 🔴 담당자 탭 17개 × 폭 3가지 (YB)
python ai-loop/check_admin_screens.py --selftest # 로그인을 안 하면 잡히는지
⚠ 두 도구의 재는 규칙은 ai-loop/_screen_probe.py 하나가 진실이다 (YB) —
문턱값(글자 11px·터치 24/44px·줄 60/80자)도 판정도 거기 있다. 새 잣대를 넣으면
고객·담당자 두 화면이 동시에 그 잣대로 재진다.
🔴 --selftest는 일부러 망가뜨려 잡히는지 본다. 고객 쪽은 픽스처를 안 꽂고,
담당자 쪽은 로그인을 안 한다 — 둘 다 아무것도 안 보고 초록이 되는 실패 모양이다.
실제로 YA에서 고객 도구가 그렇게 통과했다(오류 화면을 재고 있었다).
⚠ 담당자 화면은 폰 폭을 재지 않는다 — 요율표는 열이 12개라 고칠 수 없는 결함이
수백 건 쏟아진다. 늘 ✗인 잣대는 아무것도 말하지 않는다(CLAUDE.md).
⚠ 요율표를 실제 값으로 보려면 ai-loop/fixtures/rates_live.json이 필요하다
(curl -s https://bizpage-estimate.vercel.app/api/rates -o ai-loop/fixtures/rates_live.json).
/api/rates는 인증 없이 열리는 공개 주소다 — 고객 브라우저가 견적을 계산할 때 쓴다.
⚠ check_customer_screens.py가 보는 것은 「카톡으로 받는 견적서」다 —
check_contrast.py의 「견적서 문서」(openEstimateWindow()가 여는 인쇄용 팝업)와
다른 문서다. 고객에게 실제로 가는 것은 estimate-view.html?id=… 주소 한 줄이고,
그 화면은 YA 전까지 브라우저로 한 번도 열려 본 적이 없었다(jsdom은 색도 크기도
위치도 계산하지 않는다).
🔴 그 화면은 문서를 못 받으면 오류 화면으로 떨어지는데 거기에도 글자와 버튼이 있어
검사가 멀쩡히 통과한다 — 실제로 그렇게 통과했다. 그래서 두 도구 모두
_browser_fixtures.py의 assert_loaded()로 정말 견적서를 보고 있는지 먼저 확인한다.
픽스처(fixtures/share_doc.json)는 손으로 짓지 않고 virtual_journey.js가 실제로
만든 payload를 내보낸 것이다(--share-json=…).
⚠ check_contrast.py는 '보이는데 안 읽히는 글자'를 찾는다. 같은 결과를 내는 사고가
서로 다른 원인으로 두 번 났다 — 칸 전체에 건 opacity가 담당자가 쓴 글까지 흐리게 했고,
편집 손잡이의 흰 배경이 검은 박스 위 흰 글자를 지웠다. 둘 다 스위트를 통과했다
(jsdom은 색을 계산하지 않는다). 색·투명도·배경을 건드렸으면 이걸 돌린다.
장식용 글자와 비활성 버튼은 빼되 몇 건인지 항상 적는다(조용히 빼면 그게 거짓말이다).
⚠ 고객이 보는 쪽도 재 볼 것. 지금까지 잡힌 모양 결함은 전부 관리자 화면 밖에서도
났다 — 견적서가 휴대폰에서 가로로 289px 삐져나가고 있었는데(RH) 데스크톱에서는 0px라
아무도 못 봤다. check_quote_doc_layout.py는 프로덕션 DB에 아무것도 남기지 않는다
(로컬 파일로 열고 /api/*를 막아 견적이 전송되지 않는다).
jsdom은 레이아웃을 계산하지 않아 위 스위트로는 보이는 모양을 잴 수 없다. 실제로
"글자가 번호 칸에 갇혀 한 글자씩 세로로 쏟아지는" 상태가 배포된 적이 있는데, 그때
소스만 봐서는 아무 이상이 없어 보였다. 일정 편집 화면에서도 같은 유형으로 두 번 잡혔다 —
버튼이 좁은 화면에서 카드 밖으로 47px 밀려난 것(RD), 자동 높이가 textarea 기본 rows에
걸려 한 줄짜리 문구가 두 줄 칸을 쓰던 것(RE). 브라우저 설치가 필요해 스위트에는 넣지
않았고, 대신 그 결함의 원인이 되는 구조는 test_qN_manual.js·test_rD·test_rE가
항상 막는다.
jsdom이 --no-save로 설치돼 있어 실행 시 NODE_PATH=<프로젝트경로>/node_modules가
필요하다(run_all_tests.js는 알아서 잡는다). 턴 종료 시 Stop 훅
(ai-loop/hooks/verify_on_stop.js)이 앞의 두 개를 자동 실행한다.
node ai-loop/audit_customer_journey.js # 고객 화면 4개의 버튼을 전부 눌러 본다
node ai-loop/audit_customer_journey.js --verbose # 터짐 · 죽은 링크 · 아무 일도 안 남 · 이름 없는 칸
node ai-loop/fault_journey.js --verbose # 🔴 **서버가 죽었을 때 고객이 무엇을 보는가**
node ai-loop/smoke_prod_journey.js # 프로덕션 주력 경로 — **기본은 dry-run**
node ai-loop/smoke_prod_journey.js --live --cleanup # 🔴 운영 DB에 실제로 쓰고, 만든 행을 지운다
node ai-loop/audit_admin_journey.js # 🔴 **담당자 화면 둘**의 버튼을 전부 눌러 본다 (XT·XV)
node ai-loop/audit_admin_journey.js --mode=filled # admin.html만, 며칠 쓴 계정 상태로
node ai-loop/audit_admin_journey.js --mode=pro # admin-quote-pro.html(내부직원용 1→5단계)만
⚠ 담당자 화면은 admin.html 하나가 아니다. admin-quote-pro.html(내부직원용 견적 산출)이
고객에게 나갈 금액을 실제로 만드는 자리다 — 그래서 --mode=pro로 따로 훑는다.
⚠ 2026-09-17: 예전엔 admin-quote.html(「자동 견적 산출 · 고객용」)도 있었고 --mode=quote로
훑았다. 대표 지시로 지웠다 — 이름과 달리 담당자 도구였고, 내부직원용이 그 칸을 전부 덮는다.
🔴 고장은 반드시 난다 — 그때 사람이 보는 화면을 재는 것이 fault_journey.js다 (XX).
세 갈래를 훑는다:
· 견적을 내는 길 12가지 — 요율 500 · 저장 500 · 발급 503 · 네트워크 끊김 · DB 전체 다운
· 받은 링크를 여는 길 5가지 — 404 · 500 · 503 · 끊김 · 알맹이 없는 200
· 담당자 화면 4가지 — 견적/문의/대장/요율 조회가 죽었을 때
**감춰진 모달을 뺀 「보이는 글자」**로 무엇을 말했는지 본다. 판정은 넷:
① 아무 말도 안 하는가 ② 거짓 성공(접수 안 됐는데 「접수되었습니다」) ③ 다시 누르면
되는 것을 문의로 보내는가 ④ 못 불러온 것을 「없습니다」로 보여주는가.
지금 값 21가지 · 문제 0.
⚠ 「막힌다」를 결함이라 부르지 않는다 — 담당자 화면은 목록을 못 받으면 일부러
대시보드를 안 연다(낡은 목록으로 응대하는 것보다 낫다는 판단, PW). 볼 것은
이유를 말하는가다. 처음엔 그걸 안 재고 「못 들어간다」를 결함 2건으로 셌다.
⚠ 감춰진 모달 안의 글자로 「말했다」고 세면 아무것도 안 지킨다 — 이 도구가 처음 잡은 것이
바로 그 자리였다(발급 실패 안내가 모달 안에만 있어, 성공/실패 화면이 사실상 같았다).
⚠ 담당자 화면은 로그인부터 통과해야 아무것도 안 보인다 — _admin_fixtures.js가
그 답을 준다(빈 계정 / 며칠 쓴 계정 두 상태). 없으면 로그인 폼만 훑고 「깨끗하다」고
말하게 된다(결함 생성기 ③).
⚠ 훑는 규칙은 고객 화면과 같은 자(_journey_probe.js)를 쓴다. 두 벌이 되면
「터졌다」·「죽은 링크」·「아무 일도 안 났다」의 뜻이 갈려 두 결과를 나란히 못 본다.
node ai-loop/virtual_journey.js --n=60 # 실제로 올 법한 손님 60명 → 바탕화면 `가상견적서`
node ai-loop/virtual_journey.js --edge # 🔴 까다로운 손님 40 → `가상견적서_가장자리`
node ai-loop/virtual_journey.js --sweep # 목적지 60곳 전수 → `가상견적서_가장자리`
node ai-loop/virtual_journey.js --edge --sweep --mix --n=300 # 전부(가장자리 폴더로)
node ai-loop/virtual_journey.js --n=300 --no-docs # 견적서 파일은 안 남긴다(큰 배치용)
node ai-loop/virtual_journey.js --n=60 --no-clean # 지난 회차를 안 지우고 이어 붙인다
🔴 폴더가 둘이다 (대표 지적 2026-08-27 — 「가상견적서 폴더를 보니 질문이 이상한 게
너무 많더라구」). 맞는 말이었다: --edge --sweep 결과가 같은 폴더에 섞여, 열면
「인원 1명」·「일수 1일」·「3년 뒤 출발」이 먼저 보였다. 그건 일부러 넣은 시험 값이지
고객이 하는 질문이 아니다.
| 폴더 | 무엇이 들어가나 |
|---|---|
바탕화면\가상견적서 | 실제로 올 법한 손님 — 대표가 보시는 곳 |
바탕화면\가상견적서_가장자리 | 일부러 까다롭게 만든 시험 손님 (--edge·--sweep) |
⚠ 매 회차는 지난 회차를 지우고 시작한다(이 도구가 만든 0001_… 폴더와 보고서 셋만).
섞이면 「지금 우리 서비스가 이렇게 답한다」를 볼 수 없다. --no-clean으로 끌 수 있다.
🔴 질문이 말이 되는지는 test_xW_persona_realism.js가 지킨다 — 해외 3일 미만·
유럽 7일 미만·부산 출발 미주·휴양지 기관 방문·학교의 「임원」·조사(가이드은) 같은 것을
씨앗 세 개 450명으로 매번 검사한다. 손님을 짓는 규칙을 고치면 여기서 걸린다.
🔴 견적서 파일이 실제로 폴더에 떨어진다 (대표 지시 2026-08-27 — 「내 눈으로 직접 만들어지는 견적서를 볼 수 있게」). 손님마다 고객이 손에 쥐는 세 형태 그대로:
1. 고객이 넣은 내용.md
2. 견적서 (카톡으로 보내는 것).html estimate-view.html이 그린 정식 문서
3. 견적서 (인쇄·PDF용).html 계산 직후 인쇄하는 문서 (Ctrl+P로 인쇄 모양)
4. 견적서 (엑셀에서 여는 표).csv 결재에 붙이는 표 (BOM 있음 — 한글이 안 깨진다)
5. 자세한 기록.json
그리고 맨 위에 _모아보기.html 한 장 — 이것만 열면 손님 목록에서 바로 건너뛴다.
⚠ 저장본은 <script>를 걷어낸다. 안 걷으면 페이지 스크립트가 다시 돌면서 서버를
찾다 실패해 빈 화면이나 오류 안내로 덮어쓴다. 대신 탭 전환은 동작하지 않는다
(고른 코스 한 벌이 보인다) — 그 사실을 문서 맨 위 띠에 적어 둔다.
⚠ CSV는 화면이 쓰는 __toCsv를 그대로 부른다. 여기서 다시 만들면 BOM·따옴표 규칙이
갈라져, 정작 고객 파일이 깨져도 검사는 멀쩡하다고 말한다.
손님 한 명이 밟는 길: 홈페이지 띄우기(운영 요율 실제 수신) → 폼 채우고 제출 →
결과를 글자로 읽기 → 「견적서 받기」 → 인쇄용 팝업 읽기 → 서버의 진짜 검증기
(quote_verify.js) 통과 → 일반 문의 → 견적 기반 상담 신청 → 링크 견적서 그리기 →
🔴 두 문서가 같은 사실을 말하는지 대조.
결과는 바탕화면 가상견적서\ — 손님마다 한 폴더(요청·결과·실제로 그려진 견적서 HTML),
맨 위에 _요약.md·_문제모음.md.
⚠ 운영 DB에 아무것도 안 쓴다. 수백 건을 프로덕션에 보내면 견적번호가 수백 개
소모되고 대장이 덮인다. 요율만 운영값을 받는다. 프로덕션 왕복은 smoke_prod_journey.js가
따로 하고, 그건 소수만 --live --cleanup으로 돈다.
⚠ 이 도구가 통과했다고 「프로덕션에서 사람이 눌러 봤다」가 아니다.
🔴 무작위 300명은 전부 통과했다. 결함 일곱은 전부 --edge와 고장 주입에서 나왔다 —
계수 1.0이 아닌 견적이 전부 링크를 못 받던 것 · 감춰진 칸이 견적 버튼을 영영 막던 것 ·
출발일을 비우면 금액만 나오고 견적서가 안 오던 것 · 담당자가 지난 행사를 재견적 못 하던 것 ·
인쇄용 견적서에 유효기간이 없던 것 · 저장이 실패해도 「접수되었습니다」라고 하던 것 ·
상담 신청에 소속이 빈 채로 가던 것.
⚠ 없는 결함도 15건 만들었다가 되돌렸다 — 계수를 빼고 항목 합을 잰 것(9건),
일반 문의에 「견적 연결」을 요구한 것(6건). 길이 둘이면 기대도 둘이어야 한다.
화면을 띄우는 자리는 ai-loop/_page_boot.js 한 곳이다(도구마다 다시 짜지 말 것).
이 방식으로만 나온 결함이 여럿이다 — 고객이 견적서 링크를 한 번도 못 받고 있었고(XJ),
패키지 견적서에 일정이 한 줄도 안 실렸다(XC). 조각으로 보면 양쪽 다 멀쩡해 보인다.
⚠ 화면 글자만 본다. body.textContent에는 페이지 안쪽 <script> 소스가 통째로 들어 있어
그대로 검사하면 없는 결함이 생긴다(실제로 5건 만들었다).
git push origin master = 프로덕션 자동 배포. 별도 배포 명령이 없다.
node ai-loop/db_migrate.js)을 배포보다 먼저.
순서가 뒤바뀌면 그 기능이 500으로 깨진다. 실행은 사용자 승인 후.?action= 분기로 추가한다.
⚠ 하위 폴더도 한 칸씩 먹는다 — api/admin/*.js 셋과 api/*/[id].js 셋을 합쳐
2026-08-27 실측 12/12(맨 위 6 + 6). audit_internals.js가 이 수를 매번 센다.데이터는 코드와 다른 곳에 있다 — Neon Postgres(DATABASE_URL)다. 배포해도 데이터는
그대로고, 코드를 되돌려도 데이터는 안 돌아온다. 그래서 별도로 받아 둔다.
🔴 자동 백업이 정말 돌고 있는지부터 본다 (2026-09-03 YO에서 이걸 몰라 이틀 놓쳤다):
node ai-loop/audit_backup_health.js # 🔴 백업이 조용히 멈췄는가 — 신선도 · 빠진 날 ·
# 로그가 어떻게 끝났는가 · 작업 스케줄러 마지막 결과
⚠ 「마지막 백업이 어제」로 안심하지 말 것. 2026-09-02 백업은 돌다가 죽어 있었다
(작업 스케줄러 0xC000013A 강제 종료 · 로그가 ^C로 끊김). 이 노트북이 배터리로
돌아가면 작업이 시작조차 안 하거나 도중에 죽는 설정이었다
(DisallowStartIfOnBatteries·StopIfGoingOnBatteries — 2026-09-03에 둘 다 껐다).
로그 31회 중 7회가 정상 종료가 아니었는데 아무도 몰랐다.
db_backup.js의 stalenessNote가 그 창구였지만 --list를 부를 때만 나온다 —
결함 생성기 ③(실행된 적 없는 안전망)이다. 위 도구가 그걸 한 곳으로 모았다.
node ai-loop/db_backup.js # 16개 테이블 전체를 한 파일로 (읽기 전용)
node ai-loop/db_backup.js --list # 받아둔 백업 목록 + 노트북·클라우드 양쪽 상태
node ai-loop/db_restore.js # 되돌리기 '계획'만 출력 (기본은 아무것도 안 쓴다)
node ai-loop/db_restore.js --confirm # 계획대로 실행 — 빠진 행만 채운다
node ai-loop/setup_cloud_backup.js # 클라우드 사본 설정 — 계획만 출력
node ai-loop/setup_cloud_backup.js --apply # 실행 (구글 드라이브 폴더를 찾아 설정)
node ai-loop/db_status.js # 운영 DB 현황 — select만 한다. **연락처는 마스킹**(--full로만 원본)
node ai-loop/db_migrate.js # 스키마 변경 — 실행은 사용자 승인 후
ADMIN_BOOTSTRAP_ID=admin ADMIN_BOOTSTRAP_PW=… node ai-loop/db_seed_admin.js # 관리자 1회 시드
백업은 두 곳에 남는다 — 노트북(../비즈페이지_백업/)에 먼저 쓰고, 클라우드 동기화
폴더에 사본을 하나 더 올린다(.env.local의 BACKUP_MIRROR_DIR).
⚠ 왜 '주 저장소를 클라우드로'가 아니라 '사본을 하나 더'인가. 구글 드라이브
스트리밍 모드에서 G:는 앱이 켜져 있을 때만 존재한다. 백업 위치를 통째로 옮기면
앱이 꺼졌거나 인터넷이 없는 날엔 백업이 아예 안 된다 — 정작 노트북이 이상한 날 백업이
빠지는 셈이다. 그래서 노트북에 먼저 쓰고(항상 성공), 클라우드에 올린다(되면 올린다).
사본이 실패해도 전체를 실패로 만들지 않되, --list가 사본이 며칠째 안 올라갔는지 계속
말한다(조용히 끊기지 않게).
미러링 모드를 쓰면 사본 폴더가 평범한 로컬 폴더가 되어 앱이 꺼져 있어도 사본까지
성공한다 — 그쪽이 더 낫다.
../비즈페이지_백업/). 이 저장소는 루트가 그대로 정적
출력이라 안에 두면 공개될 수 있고, 백업에는 고객 개인정보·비밀번호 해시·가입코드가
들어 있다. 안에 쓰려면 --allow-in-repo를 명시해야 한다(그래도 gitignore가 막는다)..env.local의 BACKUP_DIR — 우선순위는 --dir > BACKUP_DIR > 기본값.
클라우드 동기화 폴더(구글 드라이브·MYBOX 등)를 여기에 넣으면 매일 백업이 자동으로
노트북 밖에도 남는다. 백업이 이 PC 한 곳에만 있으면 노트북과 함께 사라진다 —
Neon은 살아 있어도, "실수로 지운 것"을 되돌릴 수단은 이 파일들뿐이다.
경로를 저장소에 커밋하지 않는 이유: 이 PC에만 해당하는 값이라서다.
⚠ BACKUP_DIR로 지정한 폴더의 상위 경로가 없으면 폴더를 새로 만들지 않고 멈춘다.
동기화 앱이 꺼졌을 때 조용히 새 폴더를 만들면, 백업은 매일 성공하면서 클라우드에는
한 건도 안 올라가는 상태가 된다.
⚠ 클라우드에 올리면 고객 개인정보가 외부 서비스에 저장된다. 그 계정에는 반드시
2단계 인증을 걸 것.db_migrate.js에서 읽는다 — 새 테이블을 만들면 백업 대상도 자동으로 는다.PARTIAL을 붙이고 종료 코드 1로 끝낸다. 저장한
파일은 다시 읽어 행 수까지 대조한 뒤에야 성공이라고 말한다.on conflict do nothing). 테이블을
통째로 백업 시점으로 되돌리려면 --replace(그 테이블의 이후 변경은 사라진다).id가 bigserial인 테이블은 복원 후 시퀀스를 다시 맞춘다. 안 맞추면 복원 며칠 뒤
"계정 추가가 안 된다"로 나타난다.자동 실행 — Windows 작업 스케줄러에 bizpage-db-backup으로 등록돼 있다(매일 18:00,
ai-loop/backup_daily.bat, 실행 로그는 ai-loop/logs/backup.log). PC가 꺼져 있어 건너뛴
날은 다음에 켜질 때 실행된다(StartWhenAvailable).
🔴 그것만으로는 절반이 빠졌다(2026-09-14~27, 14일 중 7일) — 18:00 이후 한 번도 안 켜진 날은
「다음에 켜질 때」가 다음 날 저녁이 된다. 그래서 bizpage-db-backup-logon(로그온 3분 뒤,
--skip-if-fresh 20 — 20시간 안에 온전한 백업이 있으면 건너뜀)을 함께 둔다(2026-09-28).
또 깨어난 직후엔 인터넷이 없어 표를 못 읽으므로 20·40·60초 쉬며 다시 읽고, 부분 백업인 날은
옛 백업을 정리하지 않는다(9/26에 2 KB짜리 실패 파일이 멀쩡한 백업을 밀어냈다).
⚠ 자동 백업은 조용히 멈춘다 — 스케줄러가 실패해도 아무도 로그를 안 본다. 그래서
--list가 마지막 백업이 며칠 전인지 항상 말하고, 이틀 이상 지났거나 부분 백업이면
종료 코드 1로 끝낸다. 가끔 node ai-loop/db_backup.js --list를 확인하면 된다.
# 상태 확인 / 지금 즉시 한 번 실행 / 해제
Get-ScheduledTaskInfo -TaskName 'bizpage-db-backup'
Start-ScheduledTask -TaskName 'bizpage-db-backup'
Unregister-ScheduledTask -TaskName 'bizpage-db-backup'
둘 다 gitignore돼 있고 저장소에 커밋하지 않는다.
.env.local — 런타임·스크립트용. DATABASE_URL, SESSION_SECRET,
OPENAI_API_KEY(마케팅 인사이트), Neon/Postgres 접속값.ai-loop/.env — 도구용. OPENAI_API_KEY(GPT 협의), ANTHROPIC_API_KEY, PEXELS_API_KEY.