Lalabase
Tickets, Doku, Zeiterfassung und Kundengespräche an einem Ort, durchsuchbar per KI. Für Softwarefirmen mit Kundenprojekten. Open Source: auf deinem eigenen Server oder in unserer Cloud in Deutschland.
Translation pending
기업·공공기관·교육기관 해외연수 전문. 55개국 현지 단가 기반 즉시 견적 · 방문 기관 섭외 · 현지 완전 지원.
Translation pending
BusinessJavaScript · HTMLGitHub @peakof3366-labOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
기업 해외연수(MICE) 견적을 웹에서 즉시 산출해 고객에게 제공하는 서비스. 공개 사이트: 이 파일은 입구 안내다. 작업 규칙은 CLAUDE.md, 지금 어디까지 했는지는 git log --oneline에 있다(커밋 메시지가 곧 작업 기록이다). 역할이 겹치지 않게 유지할 것 — 상태를 문서에 따로 적기 시작하면 코드와 어긋난다. 정적 프론트 + Vercel 서버리스 함수 + Neon Postgres. 빌드 단계가 없다 — 루트가 그대로 정적 출력이라 커밋한 파일이 공개 도메인에서 읽힌다 (그래서 내부 문서·검증 도구는 .vercelignore로 업로드 자체를 막는다).Read the full 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.