turboairbrain.uk/ask — 질문응답 시스템 구축 기록
현재 상태 — 정상 운영 중 (2026-08-03~07 닷새간 13차례 후속 개선 포함)
https://turboairbrain.uk/ask— 사장님·Nathan 매니저가 브라우저에서 질문을 입력하면, Claude API(thinking 비활성화,max_tokens: 50000)가 컴파일된 TAB Wiki 전체를 근거로 답변한다. 답변은 marked.js로 표·목록까지 정상 렌더링되고, PDF 저장 버튼과 토큰/비용/누적 사용액 표시,stop_reason: "max_tokens"잘림 경고가 답변 위에 붙는다. 문답은 Cloudflare KV에 기록되고,TAB-Wiki-QuerySync작업이 10분 주기로(화면에 보이지 않는 완전 백그라운드 실행) 이를30. Queries/에 markdown으로 동기화한 뒤 KV에서 삭제 — 전체 사이트 재빌드/배포와 AI 컨텍스트 번들 갱신은TAB-Wiki-Rebuild가 1시간 주기(완전 백그라운드)로 담당. 다음 재빌드에서 그 Query Result가 위키에도 편입되어(compiled wiki가 계속 성장하는) Karpathy 패턴을 그대로 따른다.
Capture Summary
| Field | Value |
|---|---|
| Topic | 외부 임원 질문 → LLM Wiki 근거 답변 → 자동 기록 시스템 |
| Platforms | Claude Code (claude-sonnet-5) |
| Capture method | 세션 내 실행한 명령/결정 사항 수동 요약 |
| Captured at | 2026-08-03 |
Original Content
배경 — 요구사항
사용자가 명시한 요구: “우리 사업과 관련하여, 외부에서 매니져나 사장님이 질문을 했을때 지금처럼 AI 에이젼트가 이 LLM wiki 폴더를 참조하여 답변을 하고 Query 기록과 문서가 저장 되도록 만들고 싶어.”
두 가지 방향을 제시하고 사용자가 선택:
- 사이트에 질문하기 페이지 추가(Cloudflare Worker + Claude API) — 구축 필요, API 비용 발생, 기록 자동 저장 ← 선택됨
- Claude.ai Project 공유 — 구축 거의 없음, 기록 자동 저장 안 됨(수동 관리 필요)
컨텍스트 전략 결정
TAB 관련 컴파일된 Wiki(20. Wiki/ + 30. Queries/, tab-project 태그 기준) 총량을 측정: 약 43,000 토큰(24개 파일, 130KB). Raw Sources까지 포함하면 142,000 토큰으로 커짐.
결정: 별도 벡터 검색(RAG/임베딩 파이프라인) 없이, 매 질문마다 컴파일된 Wiki 전체를 Claude API 컨텍스트로 그대로 전달하는 방식 채택 — 이 볼트 자체의 설계 철학(RAG vs Compiled Wiki 개념 페이지가 이미 이 주제를 다룸)과 정확히 일치하고, 이 정도 규모에서는 검색 인프라를 별도로 구축하는 것보다 단순하고 정확도가 높음.
인프라 구축
- Cloudflare KV 네임스페이스 생성(
tab_wiki_qa, ID595f56607564432e8ee2e994b4ea2b3c) — 용도 두 가지:context:wiki-bundle키: 컴파일된 Wiki 전체 번들(재빌드마다 갱신)query:{timestamp}:{uuid}키: 질문·답변 임시 기록 (동기화 후 삭제)
- Cloudflare API로 이 KV를 Pages 프로젝트(
turboairbrain-wiki)의 production/preview 환경에TAB_WIKI_QA바인딩으로 연결 (wrangler pages secret put과 달리 KV 바인딩은 CLI 플래그가 없어 REST API로 처리). ANTHROPIC_API_KEY를 Pages Secret으로 등록.functions/api/ask.js(Cloudflare Pages Function) 작성 — 질문을 받아 Wiki 컨텍스트 + 시스템 프롬프트(확인된 사실/추정 구분, Wiki 외 내용 금지, 참고 문서 나열)와 함께 Claude API 호출, 답변 반환 + KV에 기록.static-pages/ask.html— 질문 입력 폼(이름 선택: 사장님/Nathan 매니저/기타, 질문 텍스트박스) + 답변 렌더링. Quartz 빌드 파이프라인이 관리하지 않는 영역이라 재빌드 스크립트가 매번public/ask/로 복사.rebuild-site.ps1에 두 단계 추가:- (재빌드 전) Q&A 동기화: KV의
query:키를 모두 읽어30. Queries/YYYY-MM-DD-Q-{slug}.md로 저장(Template_Query Result.md스키마 준수,askedBy/askedVia새 camelCase 키 추가) 후 KV에서 삭제. - (재빌드 후) Wiki 컨텍스트 번들 갱신:
tab-project태그가 붙은 Wiki/Queries 파일을 전부 모아context:wiki-bundleKV 키에 재업로드.
- (재빌드 전) Q&A 동기화: KV의
트러블슈팅 (재사용 가치 높은 패턴들)
-
PowerShell 실행 정책: 관리자 PowerShell에서도
npx.ps1실행이 기본적으로 막혀 있음(running scripts is disabled) —Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process -Force로 그 세션에서만 임시 허용. -
wrangler kv기본값은 로컬 시뮬레이션:wrangler kv key put/get/list/delete는--remote플래그 없이는 로컬 파일 기반 가짜 저장소에 쓴다 — 실제 배포된 Pages Function은 이걸 못 본다. 모든 KV 명령에--remote필수. -
wrangler pages secret put에는--path옵션이 없다 (wrangler kv key put에만 있음) — 착각해서 잘못된 명령을 안내했던 실수. 시크릿은 stdin 파이프(Get-Content -Raw | wrangler pages secret put ...) 또는 인터랙티브 프롬프트로만 입력 가능. -
Windows PowerShell의 숨김 입력 프롬프트가 붙여넣기를 깨뜨림: 사용자가
wrangler pages secret put의 마스킹된 “Enter a secret value” 프롬프트에 API 키를 붙여넣었으나 1글자만 등록됨 — 재현 원인 미확정이지만, 인터랙티브 마스킹 프롬프트에 긴 문자열을 붙여넣는 것 자체가 이 환경에서 신뢰할 수 없음이 확인됨. -
PowerShell → 네이티브 프로세스 파이프의 인코딩 문제:
"value" | wrangler pages secret put ...처럼 Windows PowerShell(5.1)에서 파이프로 값을 흘리면 재현 불가능한 방식으로 깨질 수 있음(이번엔 1글자로 truncate). 반면 Bash(Git Bash)에서 동일한 파이프(cat apikey.txt | npx wrangler ...)는 문제없이 동작 — 같은 시스템의 다른 셸이 훨씬 안정적이었다. 이 볼트의 다른 세션에서도 PowerShell 파이프의 BOM 오염 문제(PowerShell CLI 파이프의 BOM 오염 문제)가 이미 한 번 발견된 바 있음 — Windows PowerShell 5.1의 파이프-투-네이티브프로세스 경로 전반이 신뢰도 낮은 것으로 결론. -
Cloudflare Pages 시크릿은 소급 적용되지 않음:
wrangler pages secret put으로 시크릿을 바꿔도, 이미 배포된(live) Function은 옛날 값을 계속 사용한다 — 새 배포를 만들어야 새 시크릿이 반영됨. 이 때문에 “키를 새로 등록했는데도 여전히 1글자로 인식” 현상이 발생 — 재배포 후 해결. -
Cloudflare 엣지가 5xx 응답 본문을 자체 에러 페이지로 덮어씀: Pages Function이 의도적으로
502상태코드를 반환해도(예: “AI 호출 실패”), Cloudflare 엣지가 그 JSON 바디를 자체 “error code: 502” 일반 에러 페이지로 교체해버려 진짜 에러 메시지가 클라이언트에 전혀 안 보임. 해결: Function이 애플리케이션 레벨 에러도 항상 HTTP 200으로 반환하고, 성공/실패는 JSON 바디의ok: true/false필드로만 구분하도록 설계 변경. -
Windows PowerShell 5.1이 BOM 없는 UTF-8 스크립트 파일(.ps1)의 한글 리터럴을 오독:
rebuild-site.ps1안에 직접 작성한 한글 문자열(YAML 템플릿 등)이 실행 시 mojibake로 깨짐 — 스크립트 파일 자체를 BOM 있는 UTF-8로 재저장([System.IO.File]::WriteAllText($path, $content, [System.Text.UTF8Encoding]::new($true)))해서 해결. -
외부 프로세스(wrangler) stdout 캡처 시 코드페이지 불일치:
& npx wrangler kv key get ...로 받은 한글 JSON 값도 별도로 깨짐 —[Console]::OutputEncoding = [System.Text.Encoding]::UTF8+$OutputEncoding = [System.Text.Encoding]::UTF8를 스크립트 최상단에 명시해서 해결. 8번과 9번은 서로 다른 두 개의 별도 인코딩 버그였다는 점이 이번 세션의 핵심 교훈 — 하나만 고치면 나머지 절반은 계속 깨진 채로 남는다. -
PowerShell 5.1의
ConvertFrom-Json이 빈 배열"[]"을$null로 반환:@($null)은 원소 1개(그 원소가 null)짜리 배열이 되어버려,foreach루프가 “빈 리스트인데도” 한 번 돌면서$k.name이 빈 문자열이 되고 이후 명령이 “Not enough non-option arguments” 에러로 실패.Where-Object { $null -ne $_ }로 걸러내서 해결.
검증
/api/ask에 실제 질문(“우리 경쟁사 중 최근 보증기간을 늘린 곳이 있나요?”) POST → Claude가 보증기간 경쟁 (상업용 냉장 프리미엄 브랜드) 개념 페이지를 정확히 인용하며 답변, 확인된 사실/추정 구분, 참고 문서 목록까지 정상 생성.- KV에
query:키 생성 확인 →rebuild-site.ps1수동 실행 →30. Queries/2026-08-03-Q-Turbo-Air의-셀프클리닝-콘덴서-...md파일로 정상 동기화(한글 파일명·본문 모두 깨짐 없음, 인코딩 수정 후) → KV 키 삭제 확인. - 두 번째 질문(“Turbo Air의 셀프클리닝 콘덴서 기술이 컴프레서 고장률에 어떤 영향을 주나요?”)도 상관관계/인과관계를 명확히 구분하는 고품질 답변 확인.
후속 개선 1 — 답변 렌더링 깨짐 + PDF 다운로드
요구: “마크다운 파일이 깨져보이게 나오는 점 수정하여, 답변이 전체적으로 깔끔하게 표시될 수 있도록 수정해주고, 원한다면 pdf 파일로 해당 답변을 다운로드 할 수 있는 메뉴를 만들어줘.”
- 원인:
renderAnswer()가##/**bold**만 정규식으로 처리하고 표(|)·목록은 그대로 텍스트로 노출 — 스크린샷에서 표가 파이프 문자 그대로 깨져 보임. - 해결: CDN(
marked@12.0.2)으로 진짜 마크다운 파서 도입, 표/목록/코드블록/블록쿼트 CSS 추가. - PDF 다운로드는 html2pdf.js 등 추가 의존성 대신
window.print()+#print-view전용@media print블록으로 구현(질문/질문자/일시/답변만 인쇄, 나머지 UI는 인쇄 시 숨김) — 브라우저의 “PDF로 저장” 인쇄 대상으로 저장 가능.
후속 개선 2 — 토큰/비용/누적 사용액 표시
요구: “답변이 완료되면, 답변 박스 위에, 이 답변을 위해 사용된 토큰 수, 사용금액, 남은 크레딧 금액이 표시될 수 있도록 해줘. 가능하면 호주달러(AUD)로.”
- 조사 결과 확인: Anthropic API에는 계좌 잔액을 조회하는 공식 엔드포인트가 없음(콘솔 수동 확인만 가능) — “남은 크레딧” 표시는 기술적으로 불가능함을 사용자에게 명시하고, 대안으로 이 도구가 KV에 자체 누적 기록하는 사용액(실제 계좌 잔액과는 다름)을 제시 → 사용자 승인.
- claude-sonnet-5 가격: 도입가
2/10 (input/output, 2026-08-31까지), 2026-09-01부터3/15 — 코드에 날짜 기준 자동 전환 로직 내장. - USD→AUD 환산은 frankfurter.app(무료, 키 불필요) 실시간 조회 + KV 12시간 캐시 + 고정 환율(1.55) fallback.
- 누적 사용액은 KV(
stats:cumulative-cost)에 USD/AUD 둘 다 각 요청 시점 환율로 개별 가산 — 나중에 한 번에 환산하는 것보다 정확.
후속 개선 3 — “기타” 이름 입력 박스가 새로고침해야만 나타나는 버그
증상: “이름에서 기타를 선택하면 바로 이름을 입력해주세요 박스가 나오지 않아. 새로고침 버튼을 눌러야지만 해당 박스가 생성이 돼.”
- 근본 원인: Quartz의 SPA 라우터(
quartz/components/scripts/spa.inline.ts)가 사이드바 링크 클릭을 가로채micromorph로 DOM만 교체하고<script>태그는 재실행하지 않음 —/ask는 Quartz 콘텐츠 파이프라인 밖의 수제 정적 HTML이라 이 계약(컴포넌트가nav커스텀 이벤트를 듣고 재초기화)을 따르지 않으므로, SPA 경유 진입 시askerSelect의change리스너 자체가 붙지 않음. 새로고침(완전한 페이지 로드)만 정상 동작한 이유. - 해결: Quartz가 인식하는
data-router-ignore속성(spa.inline.ts의getOpts()가"routerIgnore" in a.dataset이면 가로채기를 건너뜀)을 사이드바 Ask 링크에 추가 —rebuild-site.ps1의 사이드바 주입 HTML 수정. - 동일한 문제를 안고 있던 footer의 중복 Ask 링크(1차 시도 때 만든 것)는 Quartz footer 컴포넌트가 링크별 속성을 지원하지 않아 고칠 수 없어 제거(
quartz.config.default.yaml).
후속 개선 4 — 질문 즉시 로컬 반영 (폴링 vs 진짜 push 트레이드오프)
1차 요구: “30. Queries 와 내 랩탑 로컬 옵시디언의 폴더에는 언제 업데이트 되는거야? 가능하다면 질문이 생성되는 즉시 반영되도록 해줘.” 2차 정정: “스케쥴러를 통해 계속 확인하는 방식 말고… 사이트 쪽은 기존 6시간 스케쥴 유지, 로컬 옵시디언만 바로 확인 가능했으면 좋겠어.”
- 핵심 제약 설명: Cloudflare Worker(클라우드)는 랩탑 로컬 파일시스템에 직접 쓸 수 없음 — 진짜 이벤트 push를 하려면 랩탑에 24시간 상시 리스너 프로세스(+Cloudflare Durable Objects, 유료 플랜 $5/월) 또는 인바운드 터널이 필요. AskUserQuestion으로 “초단위 폴링(권장)” vs “진짜 push(고비용/고복잡도)” 트레이드오프를 제시 → 사용자가 폴링 방식 선택.
- 전체 재빌드(
rebuild-site.ps1)에서 KV 동기화 로직만 떼어낸 경량 스크립트sync-queries.ps1신설. TAB-Wiki-QuerySync예약 작업을 1분 주기로 신규 등록(기존TAB-Wiki-Rebuild는 6시간 그대로 유지) —MultipleInstances: IgnoreNew로 중복 실행 방지.- 버그 발견: 테스트 질문이
[테스트]로 시작 → 대괄호가 살아남은 슬러그로 파일 경로 생성(...-Q-[]-..md) → PowerShell이[/]를 와일드카드로 해석해Set-Content -Path의 동적 파라미터(-Encoding)가 바인딩 자체를 실패시키는 잠재 버그 발견(sync-queries.ps1·rebuild-site.ps1둘 다 동일 결함, 실제 사용자 질문이 처음으로 대괄호를 포함하기 전까진 한 번도 발현 안 됐던 잠복 버그). 슬러그 정규식에[ ] { } \`` 추가 제거 + 모든 경로 연산을-LiteralPath`로 전환해 근본 해결. - 2차 버그(사용자 지적): “스케쥴러가 계속 돌아가면 CLI 창이 켜져서 불편” — 실제로는 Task Scheduler 자체는 완전 백그라운드지만,
powershell.exe -WindowStyle Hidden은 Windows에 따라 conhost가 창을 만들었다가 숨기는 찰나의 깜빡임이 있을 수 있고 1분 주기(하루 1440회)에서는 누적되면 거슬릴 수 있음 →wscript.exe+ VBScript(WScript.Shell.Run(cmd, 0, True))로 완전 숨김 실행 래퍼(sync-queries-silent.vbs)를 만들어 예약 작업 Action을 교체 — 프로세스 생성 자체가 창 없이 이루어지는 표준 Windows 트릭.
후속 개선 5 — Query 콜아웃 줄바꿈 시 빈 줄 제거
증상: “질문:, 질문자:, 일시: 가 줄바꿈될 때 한 줄 공백이 나타나고 있어. 해당 빈 라인이 안 생기도록 해줘.” (스크린샷 첨부)
- 원인: 줄바꿈을 위해 넣었던 빈
>라인이 Obsidian/마크다운에서 별도 문단으로 렌더링되어 그 사이에 여백 발생. - 해결: 빈
>라인 대신 각 줄 끝에<br>삽입 — 같은 문단 안에서 줄만 바뀌고 문단 간 여백 없음.sync-queries.ps1/rebuild-site.ps1템플릿 수정 + 기존 생성된 Query Result 4개 파일도 동일하게 소급 수정. - 라이브 사이트 렌더링 HTML을 직접 fetch해
<p>...<br/>...<br/>...</p>단일 문단 구조로 렌더되는 것을 확인(이전엔<p>3개로 분리되어 있었음).
후속 개선 6 (2026-08-04) — 답변이 표 중간에서 잘리는 버그
증상: 사용자가 “우리회사의 호주내 마켓 포지션을 분석해 주고, 해당 포지션의 적절한 마케팅 전략을 LLM Wiki 자료 및 웹문서등을 활용하여 분석해줘”라는 질문을 보냈는데, 답변이 경쟁사 비교 표 중간에서 끊김.
- 근본 원인 (두 가지 겹침): (1)
max_tokens: 2000으로 고정 — 표 포함 상세 분석에는 부족. (2) claude-sonnet-5는thinking파라미터를 생략하면 기본적으로 adaptive thinking이 켜지고, 이 thinking 토큰도max_tokens한도 안에 함께 카운트됨 — 보이는 답변용으로 쓸 수 있는 토큰이 2000보다 훨씬 적었던 것이 실제 원인. - 해결:
thinking: { type: "disabled" }명시(이 앱은 위키 근거 종합이 목적이라 다단계 추론 불필요) +max_tokens2000 → 6000 → (사용자 요청으로) 50000까지 순차 상향. - 부가 개선: Claude API는
max_tokens에 도달하면stop_reason: "max_tokens"를 응답에 포함해 조용히 자르지 않고 알려주는데, 기존 코드는 이 값을 무시하고 있었음 —truncated플래그로 프론트엔드까지 전달해 잘림 발생 시 “⚠️ 답변이 잘렸을 수 있습니다” 경고를 표시하도록 개선. - 검증: 동일 질문 재전송 →
truncated: false, 출력 토큰 3541개로 “참고 문서” 섹션까지 자연스럽게 완결된 답변 확인. - 비고:
max_tokens: 50000은 스트리밍 없는 단일 요청 구조라, 실제로 그만큼 길게 생성될 경우 Cloudflare Pages Function이나 브라우저 fetch 타임아웃에 걸릴 이론적 위험이 있음 — 아직 실측되지 않은 리스크(비용은 실제 생성 토큰 수에 비례하므로 한도 자체를 올린다고 자동으로 늘지는 않음).
후속 개선 7 (2026-08-04) — 재빌드 주기 단축 + TAB-Wiki-Rebuild CLI 창 숨김
배경: 사용자가 “6시간→5분/1분으로 줄이면 비용·자원·랩탑 사용에 어떤 영향이 있는지” 질문 → 실측 로그 기반(재빌드 1회 평균 5570초, 매번 전체 143개 페이지 재빌드) 분석 제공:
- 5분 주기: 하루 ~5시간 CPU 사용(현재 4분/일 대비 급증), Cloudflare 배포·KV 쓰기 횟수도 월 수천 건으로 증가.
- 1분 주기: 실행 간격(60초)이 실행 시간(~62초)보다 짧아
IgnoreNew에도 불구하고 사실상 거의 쉬지 않고 도는 상태가 됨. - 결론 및 사용자 선택: 극단값 대신 1시간으로 절충.
구현:
TAB-Wiki-Rebuild예약 작업 주기를 6시간 → 1시간으로 변경.TAB-Wiki-QuerySync에 이미 적용했던wscript.exe+ VBScript 완전 숨김 실행 패턴을 재사용해rebuild-site-silent.vbs신설,TAB-Wiki-Rebuild의 Action도 교체.- 버그 발견 (신규,
.ps1과 정반대):.vbs파일을.ps1과 같은 방식으로 UTF-8 BOM 있게 저장했더니cscript가"(1, 1) Microsoft VBScript compilation error: Invalid character"로 즉시 실패 — VBScript(Windows Script Host)는.ps1과 반대로 BOM이 있으면 파싱 자체가 깨진다. BOM 없는 UTF-8로 재저장해 해결. 이 볼트에서 이미 정리된 “PowerShell 5.1 CLI 트러블슈팅” 카탈로그와는 정반대 방향의 함정이라 별도로 기록할 가치가 있음. - 검증: 예약 작업을 수동 트리거해
LastTaskResult: 0(성공) +rebuild.log에 정상 완료 기록 확인.
후속 개선 8 (2026-08-04) — 로컬 동기화 주기 1분 → 10분
요구: “https://turboairbrain.uk/ask/ 사이트에서 입력된 질문과 답변을 내 로컬 옵시디언에 동기화 하는 기간이 현재는 1분으로 되어 있는데 10분으로 변경해줘.”
TAB-Wiki-QuerySync의RepetitionInterval만 1분 → 10분으로 변경 (Action,MultipleInstances: IgnoreNew등 나머지 설정은 그대로 유지).- 후속 개선 4에서 이미 “즉시 push가 아니라 폴링”이라는 트레이드오프를 사용자가 인지·승인한 상태였으므로, 폴링 주기를 더 늘리는 것은 그 결정의 연장선 — 최대 지연이 1분에서 10분으로 늘어난다는 것 외 구조 변경 없음.
후속 개선 9 (2026-08-05) — 매출/판매실적 질문에 Google Sheets 원본 라이브(live) 조회 연결
요구: 사용자가 실제로 /ask에 “2025년 상반기와 2026년 상반기 영업/판매실적을 Google Sheets Sales Data 원본을 직접 조회하여 비교 분석해줘”라고 질문했는데, 봇이 영업판매실적 조회 절차를 정확히 인용하며 “Google Drive 커넥터는 Claude Desktop/Code 세션에만 있고, 이 Cloudflare Worker(순수 HTTPS Claude Messages API 호출)에는 그 커넥터가 없다”고 정직하게 답변 — 이를 본 사용자가 “매출/판매실적 관련 질문을 받았을 때 Google Sheets 원본을 직접(live) 조회 할 수 있도록 시스템을 수정해줘”라고 명시 요청.
핵심 설계 문제: 영업판매실적 조회 절차가 전제하는 “Google Drive 커넥터”는 Claude Desktop/Code 같은 특정 클라이언트 세션에만 존재하는 MCP 도구이고, turboairbrain.uk/ask는 Cloudflare Worker가 Anthropic Messages API를 raw HTTPS로 직접 호출하는 구조라 그 커넥터에 접근할 방법이 전혀 없다. 따라서 Worker가 자체적으로 Google Sheets API를 호출할 수 있는 별도 인증 경로가 필요했다.
구현:
functions/api/_google-sheets.js신규 파일 — Google 서비스 계정을 이용한 JWT-bearer OAuth2 플로우를 Cloudflare Worker의 Web Crypto API(crypto.subtle.importKey/sign, RSASSA-PKCS1-v1_5/SHA-256)만으로 직접 구현(외부 라이브러리 없음). 발급받은 access token은 KV에 만료시각 기준으로 캐시.- 질문에 매출/판매/영업/딜러/rebate 등 키워드가 있으면(
isSalesQuestion) Google Sheets APIspreadsheets.values.get으로Sales Data탭(16,800행+)을 직접 fetch. - 서버사이드 사전집계: 원본 시트가 너무 커서 Claude 컨텍스트에 그대로 넣을 수 없으므로, Worker가 영업판매실적 조회 절차와 완전히 동일한 규칙(Status 빈값/Cancelled/Hold 제외, 매출은 Inv Date 기준·주문건수는 Order date 기준 월 귀속, TA QLD 포함/제외 분리)으로 월별 요약표까지 집계한 뒤에만 Claude에 전달 — “Worker가 계산, Claude는 해석”이라는 역할 분리 유지.
functions/api/ask.js에 이 모듈을 연결 — 실시간 조회 성공 시 “LIVE SALES DATA” 블록을 시스템 프롬프트에 주입(Wiki 캐시보다 우선 근거로 취급하도록 프롬프트 규칙에 명시), 실패 시에도 과거 Wiki 스냅샷 수치를 최신 수치인 것처럼 제시하지 않도록 실패 사실 자체를 프롬프트에 명시하는 별도 블록을 주입.salesDataStatus(fetched/error/not_applicable/empty) 필드를 KV 로그와 API 응답에 추가.static-pages/ask.html에salesDataStatus에 따라 초록(✅ 실시간 반영됨)/빨강(⚠️ 조회 실패) 배지를 답변 위에 표시하는 UI 추가.
Google Cloud 설정 (사용자 진행): (1) GCP 프로젝트 생성, (2) Google Sheets API 사용 설정, (3) 서비스 계정 생성(tab-wiki-sheets-reader@turboair-brain.iam.gserviceaccount.com), (4) JSON 키 발급, (5) 대상 Google Sheets(“Turbo Air”)에 그 서비스 계정 이메일을 뷰어로 공유. 발급된 JSON 키 파일을 로컬에 저장해 전달받은 뒤, client_email/private_key만 추출해 wrangler pages secret put으로 Cloudflare Secret(GOOGLE_SERVICE_ACCOUNT_EMAIL, GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY)에 등록하고 로컬 JSON 키 파일은 즉시 삭제.
버그 발견 — Cloudflare Pages 시크릿의 소급 미적용: 시크릿 등록 직후 첫 테스트에서 salesDataStatus: "error" + “서비스 계정 인증 정보 미설정” 오류가 재현됐다. Google 인증·시트 fetch 로직 자체는 Node에서 독립적으로 재현한 결과 완전히 정상 동작(16,814행 정상 조회)했으므로, 문제는 Worker 코드가 아니라 배포 타이밍이었다: wrangler pages secret put으로 시크릿을 바꿔도 이미 떠 있던(live) 배포는 옛 값(빈 값)을 계속 참조하고, 새 시크릿은 그 이후에 새로 만들어지는 배포부터만 반영된다. rebuild-site.ps1을 한 번 더 실행해 새 배포를 올리자 즉시 해결 — 이 패턴은 후속 개선 6의 “Cloudflare Pages 시크릿은 소급 적용되지 않음”(트러블슈팅 6번) 항목과 근본적으로 동일한 함정이 이번엔 Google 시크릿에서도 재현된 사례.
검증: 재배포 후 사용자가 동일 질문(“2025년 상반기 vs 2026년 상반기”)을 다시 실행 → salesDataStatus: "fetched"로 실제 라이브 수치 반환 확인. 2025년 상반기 매출 1,607,491 AUD → 2026년 상반기 1,082,803 AUD(TA QLD 포함, −32.6%), TA QLD 제외 시 1,438,841 → 719,934 AUD(−50.0%), 주문건수는 −2.7%(전체)/−4.2%(TA QLD 제외)로 매출 감소폭보다 훨씬 완만 — 매출-건수 괴리를 근거로 “건당 단가 하락 또는 백로그(미인보이스) 가능성”을 확인된 사실과 구분해 해석까지 제시한 고품질 답변 확인. 결과는 2026-08-05-Q-2025년-상반기와-2026년-상반기-영업판매실적을-Google-Sh-3로 자동 동기화됨.
후속 개선 10 (2026-08-05) — Power BI 판매실적 대시보드를 사이트 안에 임베드 (Sales Dashboard 페이지)
계기: 사용자가 05_SalesReport 볼트의 Power BI 대시보드를 Power BI “웹에 게시(Publish to Web)” 기능으로 공개 링크화해 쓰고 있었는데, 이 방식이 접근 제어가 전혀 없는 완전 공개 URL이라는 점을 스스로 위험하다고 판단 — turboairbrain.uk 안에 새 페이지를 만들어 그 대시보드를 표시하고, 루트 페이지 사이드바(탐색기 위)에 진입 버튼을 추가해달라고 요청.
보안 트레이드오프 먼저 확인: “웹에 게시” 링크를 그대로 iframe에 넣고 Basic Auth로 보호되는 우리 사이트에 올려도, 그건 우리 사이트를 거치는 경로만 막을 뿐 원본 Power BI URL 자체는 여전히 완전 공개라는 점(브라우저 개발자도구로 iframe src만 보면 누구나 직접 접속 가능)을 먼저 설명 — “진짜로 막기”(Azure AD 앱 등록 + 서비스 주체 임베드 토큰, §6 Google 서비스 계정과 유사한 구조, 설정 부담 큼)와 “빠른 임시 조치”(공개 링크를 그대로 iframe, 실질적 보안 개선 없음) 중 사용자가 AskUserQuestion으로 후자를 명시 선택.
구현:
static-pages/dashboard.html신규 —/ask와 동일한 색상/폰트 팔레트로 제작, 상단에 “이 방식은 완전한 보안이 아니다”를 알리는 경고 배너 고정 삽입, Power BI 게시 크기(600×373.5) 비율을 유지하는 반응형padding-bottom트릭으로 iframe 감쌈.rebuild-site.ps1에/ask와 동일한 복사 패턴(2.52) 반복 —static-pages/dashboard.html→public/dashboard/index.html.- 사이드바 버튼 injection(§5 기존 로직, 2.55)을 확장해 ”📊 판매실적 대시보드” 버튼을 ”💬 질문하기” 버튼 바로 아래에 함께 삽입(같은
<style>/치환 패스에서 두 버튼을 한 번에 처리 — 순차로 따로 치환하면 첫 치환이 앵커를 바꿔 두 번째가 실패할 수 있어 원자적으로 묶음). - 검증: 335개 페이지 전체에 두 버튼 정상 주입 확인,
/dashboard라이브 접속 확인.
후속 미세조정 (사용자 요청 3건, 같은 날):
- 사이드바 버튼 라벨 “판매실적 대시보드” → “Sales Dashboard”(영문)로 변경,
dashboard.html의<title>/<h1>/iframetitle속성도 동일하게 통일. - 사이드바 여백 축소: 검색/다크모드 툴바 → 질문하기 버튼 → Sales Dashboard 버튼 → 파일 탐색기 사이 간격이 너무 넓다는 지적 → 원인 확인: Quartz의
.sidebar.left가 flexboxgap: 1.2rem으로 모든 직계 자식 사이 간격을 일괄 적용하고 있었음(버튼 자체margin은 부수적) → 버튼margin을 0으로 비우고.page #quartz-body .sidebar.left { gap: 0.5rem !important; }로 오버라이드해 해결. - “TAB Project Wiki로 돌아가기” 링크 추가:
dashboard.html상단에← TAB Project Wiki링크 삽입 → 대상 경로 선정 시, MOC-TAB 프로젝트의 정식 Quartz 경로(/20.-wiki/24.-maps/moc-tab-프로젝트, 한글 슬러그)보다 그 페이지의 frontmatteraliases(MOC-TAB Project)에서 Quartz alias 플러그인이 자동 생성한 ASCII 리다이렉트 슬러그/moc-tab-project(meta refresh +noindex)를 채택 — 하드코딩 링크에 안전한 짧은 영문 경로 사용.
검증: 매 변경마다 rebuild-site.ps1 재실행 → rebuild.log에 “Copied dashboard.html” / “Injected Ask + Dashboard sidebar buttons into 335 page(s)” / Deploy exit code: 0 확인, 빌드된 public/index.html에서 “Sales Dashboard” 문자열과 sidebar.left { gap: 0.5rem 규칙이 실제로 반영됐는지 grep으로 직접 확인.
후속 개선 11 (2026-08-05) — 수동 즉시 동기화 트리거 (더블클릭 파일)
요구: “지금은 내 로컬 옵시디언 폴더를 https://turboairbrain.uk/ 에 한시간에 한 번씩 동기화 시키는데, 필요할때는 수동으로 즉시 동기화 시킬 수 있도록 파일또는 버튼을 만들어 주던지, 방법을 알려줘.” — 후속 개선 7에서 TAB-Wiki-Rebuild를 6시간→1시간 주기로 좁혔지만, 급한 반영이 필요할 때 최대 1시간을 기다려야 하는 문제는 남아있었음.
설계 판단: 새 스크립트를 만들어 rebuild 로직을 중복시키지 않고, 이미 등록된 TAB-Wiki-Rebuild 예약 작업 자체를 schtasks /run /tn "TAB-Wiki-Rebuild"로 즉시 트리거하는 방식을 선택 — 로직이 한 곳(rebuild-site.ps1)에만 존재해 유지보수 시 갱신 누락 위험이 없고, MultipleInstances: IgnoreNew 중복 실행 방지·rebuild.log 기록도 자동 예약 실행과 완전히 동일하게 적용된다.
구현:
sync-now.vbs신규(C:\Users\David\quartz-site\보관본 + 바탕화면 복사본) — 후속 개선 7의wscript.exe+WScript.Shell.Run(cmd, 0, True)완전 숨김 실행 패턴과 BOM 없는 UTF-8 저장 규칙(같은 후속 개선 7에서 발견된.vbs함정)을 그대로 재사용.schtasks /run은 작업을 “시작 요청”만 하고 즉시 반환한다(실제 재빌드 완료를 기다리지 않음) — 사용자가 진행 상황을 알 수 있도록 트리거 직후MsgBox로 “동기화를 시작했습니다 (백그라운드, 완료까지 약 1분)” 안내를 띄움.objShell.Run(..., 0, True)의True(대기)는schtasks.exe명령 자체의 즉시 반환을 기다리는 것이지 재빌드 완료를 기다리는 게 아님 — 이 구분을 명확히 해야 MsgBox가 “완료됐다”가 아니라 “시작됐다”로 정확히 안내된다.- 바탕화면 파일은 원본(
quartz-site/sync-now.vbs)의 복사본 — 바탕화면 파일이 실수로 삭제돼도 원본에서 다시 복사하면 복구됨.
검증: 새로 만든 .vbs 두 파일(원본+바탕화면 복사본) 모두 xxd로 헤더 바이트 확인 → BOM 없음(27 20 = ' 로 정상 시작) 확인. 문법은 후속 개선 7에서 이미 실전 검증된 동일 패턴(sync-queries-silent.vbs/rebuild-site-silent.vbs)을 그대로 따르므로 별도 dry-run 없이도 신뢰도 높음(실제 트리거는 사용자 몫으로 남김 — 테스트 삼아 실행하면 진짜 배포가 발생하므로).
추가 버그 발견 및 수정 (같은 날, 사용자 실사용 중 발견): 사용자가 실제로 더블클릭했더니 MsgBox 안내창의 한글이 전부 깨져 나옴(스크린샷 첨부, µ¿±âÈ... 식 mojibake). 두 단계로 진단:
- 1차 시도(실패): “BOM 없는 UTF-8” 저장이 원인이라 추정 — VBScript는 BOM 없는 파일을 시스템 기본 코드페이지(한국어 Windows는 CP949)로 읽으므로, UTF-8 대신 CP949로 재인코딩해서 재배포. 하지만 여전히 깨짐.
- 근본 원인 특정:
[System.Text.Encoding]::Default(Windows-1252)와Get-WinSystemLocale(ko-KR)를 대조 확인한 결과, 이 컴퓨터의 **“유니코드가 아닌 프로그램의 언어”(시스템 로케일)가 실제로는 한국어가 아니라 영어(1252)**로 설정되어 있음을 확인 — VBScript의MsgBox는 파일 인코딩과 무관하게 항상 이 시스템 로케일로 텍스트를 해석하므로, CP949로 저장해도 1252로 잘못 해석되어 깨진 것. 즉 VBScriptMsgBox로 한글을 안정적으로 표시할 방법은 이 환경에 애초에 없음(파일 인코딩을 아무리 바꿔도 시스템 로케일이 한국어가 아니면 소용없음). - 최종 해결 — 책임 분리:
sync-now.vbs는 한글 문자열을 아예 담지 않고(objShell.Run "powershell.exe ... -File sync-now.ps1", 0, True) 실행 트리거 역할만 하도록 축소. 실제 안내창은 신규sync-now.ps1(UTF-8 BOM 있음, 이 볼트의 기존.ps1규칙 그대로)이System.Windows.Forms.MessageBox(진짜 Win32 유니코드 API, 시스템 ANSI 로케일에 영향받지 않음)로 표시하도록 이전..vbs(BOM 없음, ANSI 기반이라 유니코드 텍스트에 근본적 한계) →.ps1(BOM 있음,.NET MessageBox로 완전한 유니코드) 2단 구조로 각 스크립트 언어가 잘하는 역할만 맡김.
- 검증:
sync-now.vbs(BOM 없음) /sync-now.ps1(BOM 있음) 헤더 바이트xxd로 재확인, 사용자가 바탕화면 파일을 다시 실행해 안내창이 정상 표시됨을 직접 확인. - 재사용 가치: 이 문제는 “
.vbs는 BOM이 있으면 안 된다”(후속 개선 7)는 기존 교훈만으로는 못 잡는, 한 단계 더 깊은 함정이다 — BOM 유무는 파싱 가능 여부만 결정하고, 비ASCII 텍스트의 정확한 렌더링은 시스템 로케일에 달려 있어 VBScript 자체의 구조적 한계다. 일반화된 원칙: VBScript로 한글(또는 임의의 non-ASCII) 텍스트를 사용자에게 보여줘야 한다면, VBScript는 조용한 트리거 역할만 맡기고 실제 텍스트 표시는 PowerShell + .NET(유니코드 네이티브)에 위임한다.
후속 개선 12 (2026-08-06) — 오늘 질문이 로컬에 동기화되지 않은 원인 진단 + npx wrangler 신뢰성 근본 수정
요구: “내가 오늘 https://turboairbrain.uk/ask/ 에서 질문한 내용이 왜 아직까지 내 로컬 옵시디언에 동기화 되지 않았는지 원인을 파악해줘.”
진단 과정:
Get-ScheduledTaskInfo -TaskName 'TAB-Wiki-QuerySync'→LastRunTime: 2026-08-06 01:02:52, 그 이후NextRunTime이 이미 지난 과거 시각으로 표시 — 10분 주기 작업이 그 시점 이후로 실제 실행되지 않은 것으로 추정(랩탑 절전/종료 가능성).sync-queries.log확인 → 2026-08-05 밤부터A request to the Cloudflare API (.../keys) failed에러가 여러 차례 반복 기록됨(10:12, 13:42, 14:42, 20:03), 그 이후로는 로그 자체가 끊김.npx wrangler kv key list ... --remote를 직접 실행 → 실제로는 정상 동작해 오늘 질문 2건(query:2026-08-06T02:28:44...,query:2026-08-06T02:31:42...)이 KV에 그대로 남아있는 것을 확인. 단, 실행 시npm warn exec The following package was not found and will be installed: wrangler@4.119.0경고가 매번 출력됨을 발견.grep wrangler package.json→ 이 프로젝트에wrangler가 정식 의존성(devDependency)으로 고정돼 있지 않음을 확인 — 즉 지금까지 모든npx wrangler ...호출(재빌드·동기화 스크립트 전부)이 매번 npm 레지스트리에서 wrangler를 새로 내려받아 왔다는 뜻. 이것이 간헐적 API 실패의 근본 원인으로 특정됨 — 네트워크 상태에 따라 매 실행마다 새로 fetch를 시도하는 구조는 본질적으로 불안정하다.
해결:
sync-queries.ps1을 수동 재실행 → 이번엔 성공, 오늘 질문(“우리가 사업을 진행하면서 관리해야할 KPI들을 시기적으로 구분하여 추천”)30. Queries/에 정상 동기화 확인.- 근본 수정:
cd quartz-site && npm install --save-dev wrangler—package.json에"wrangler": "^4.119.0"으로 정식 고정. 이후npx wrangler --version재실행 시 “will be installed” 경고 없이 즉시 로컬 버전(4.119.0)을 사용하는 것으로 확인 — 앞으로는rebuild-site.ps1/sync-queries.ps1양쪽 모두 매 실행 시 네트워크 레지스트리 조회 없이 로컬 설치본을 즉시 쓰게 되어, 이런 유형의 간헐적 실패 재발 가능성이 크게 낮아짐.
재사용 가치: npx {tool}을 프로젝트 의존성으로 고정하지 않고 반복 실행하는 스케줄된 자동화(Task Scheduler 등)는, 그 도구가 로컬 캐시에 없을 때마다 매번 레지스트리 fetch를 시도하는 숨은 신뢰성 리스크를 안고 있다 — 자동화 스크립트에서 쓰는 CLI 도구는 package.json에 명시적으로 고정하는 것이 원칙.
후속 개선 13 (2026-08-07) — “동기화 버튼을 눌러도 반영 안 됨” 진단: 콘텐츠 YAML 오류가 빌드 자체를 조용히 막고 있었음
요구: “TAB Wiki 지금 동기화.vbs 파일이 잘 작동하지 않는 것 같아. 해당 파일을 실행해도 바로 동기화가 되지 않아. 해당 파일 오류를 점검해줘.”
진단 과정:
sync-now.vbs/sync-now.ps1자체 코드와 인코딩을 재점검 — 정상(후속 개선 11·12에서 이미 검증된 상태 그대로).Get-ScheduledTaskInfo -TaskName 'TAB-Wiki-Rebuild'→LastTaskResult: 0(성공)이었지만, 이는 작업이 “실행됐다”는 뜻일 뿐 “빌드가 성공했다”는 뜻이 아님을 놓치지 않고rebuild.log를 직접 확인.rebuild.log에서 **10:46:42부터 모든Build exit code가1(실패)**로 찍혀 있는 것을 발견 — 즉 사용자가 버튼을 누른 시점(11:46) 포함, 그날 오전 마지막 성공 배포(09:47) 이후 사이트가 전혀 갱신되지 않고 있었음.- 빌드 로그의 정확한 에러 메시지:
Failed to process markdown ...: unknown escape sequence (4:45)— Quartz(마크다운/YAML 파서)가 특정 Raw Source의 frontmatteraliases:필드에서\|(백슬래시+파이프)를 유효하지 않은 YAML 이스케이프 시퀀스로 거부하며 빌드 전체를 중단시키고 있었음.
근본 원인: 바로 앞 턴(/ingest)에서 32개 소스를 Raw Source로 일괄 변환하는 배치 스크립트를 짤 때, 원본 마크다운 표(## Metadata 테이블) 안에서 파이프 문자를 이스케이프하려고 썼던 \|(마크다운 테이블 셀 안에서는 올바른 이스케이프)를 그대로 YAML aliases: 필드로 복사했음 — 마크다운 표 안에서는 필요한 이스케이프가 YAML 문자열 안에서는 무효 문법이라는 점을 간과. 제목에 파이프(|)가 원래 포함된 소스 5건(예: “SilverChef | Adgemis Refrigeration”)에서만 발현.
해결:
- 영향받은 5개 Raw Source(
2026-08-07-Commercial-Refrigeration-Rebates-2026-Energy-Rebate-Check.md외 4건)의aliases:필드에서\|→|로 직접 수정(Edit 도구 사용). - 1차 시도 실패 기록: 처음엔 Bash에서 Node.js 정규식(
fm.replace(/\\\|/g, '|'))으로 5개 파일을 일괄 수정하려 했으나, Bash 이중따옴표 문자열 안에 JS 정규식 리터럴을 중첩시키는 다중 이스케이프 레이어(bash\\→\, bash가 인식 못하는\|는 그대로 보존 → 최종적으로 node가 받는 정규식이/\\|/가 되어|가 이스케이프된 문자가 아니라 정규식 alternation 연산자로 해석됨)로 인해 파일에 타임스탬프만 갱신되고 실제 내용은 안 바뀌는 조용한 실패가 발생 — 재빌드해도 여전히 같은 에러로 실패해서 발각. Edit 도구(문자열 그대로 치환, 정규식/쉘 이스케이프 레이어 없음)로 직접 고치자 즉시 해결. - 재빌드 실행 →
Build exit code: 0,Deploy exit code: 0확인, 사이트 정상 배포 확인.
재사용 가치:
- “버튼이 작동한다” ≠ “동기화가 성공한다” —
LastTaskResult: 0은 작업이 트리거되고 완주했다는 뜻일 뿐, 그 작업 내부의quartz build가 성공했는지는 별개로 확인해야 한다. 사용자가 “동기화가 안 된다”고 보고하면 먼저rebuild.log의 최근Build exit code/Deploy exit code라인을 확인하는 것이 가장 빠른 1차 진단. - 마크다운 표 안의 이스케이프(
\|)를 YAML 필드로 그대로 복사하지 않는다 — 배치 변환 스크립트를 짤 때 “마크다운 문맥에서 유효한 이스케이프”와 “YAML 문맥에서 유효한 이스케이프”를 같은 문자열로 취급하면 안 된다./lint가 이런 프론트매터 문법 오류까지는 잡아내지 못했다는 점도 확인됨(Step 8 v2/v4/v5 커버리지 체크는 하지만 YAML 유효성 자체는 검증하지 않음) — 향후/lint나validate-raw-source.sh훅에 YAML 파싱 유효성 검사를 추가할 가치가 있음(미착수, Open Question). - Bash로 Node.js 정규식을 이중따옴표 문자열에 중첩시키지 않는다 — 이스케이프 레이어가 2~3겹 겹치면(bash → node 문자열 → 정규식) 의도한 문자와 다른 문자가 최종 정규식에 도달할 수 있고, 최악의 경우 “조용히 실패”(파일은 쓰였지만 내용은 안 바뀜)한다. 이런 다중 파일 치환은 Edit 도구로 파일별 직접 치환하거나, 최소한 수정 직후 Read 도구로 재확인하는 습관이 필요.
후속 개선 14 (2026-08-12) — 방문자 피드백/메시지 기능 (사이트 → 이메일 + 로컬 옵시디언)
요구: “나는 https://turboairbrain.uk/ 사이트를 직원들에게 오픈하여 현재 피드백을 받고 있어. … 사용자가 페이지를 보고 운영자에게 메세지를 전달할 수 있도록 메뉴 또는 박스를 개발해줘. 루트 페이지와 질문하기 페이지에 각각 해당 메뉴가 있었으면 좋겠어. 해당 메세지는 내 이메일(david@turboairinc.com.au) 에서도 확인 할 수 있고, 내 옵시디언에도 새로운 폴더를 만들어 … 로컬 옵시디언에서도 확인 이 될 수 있었으면 좋겠어.”
설계: /ask의 Q&A 파이프라인(§1~§4)을 그대로 재사용 — Cloudflare Pages Function이 폼 제출을 받아 KV를 source of truth로 먼저 저장하고, 로컬 예약 작업(QuerySync 10분 · Rebuild 1시간)이 KV를 읽어 마크다운으로 떨어뜨리는 동일 구조. Q&A가 query: 프리픽스·30. Queries/였다면, 피드백은 feedback: 프리픽스·50. Site Feedback/(신규 폴더).
구현:
functions/api/feedback.js신규 — always-HTTP-200 계약(§1의 Cloudflare 엣지 5xx 덮어쓰기 우회), honeypot(website숨김 필드) 봇 차단, 메시지 5000자 제한, KVfeedback:{ts}:{uuid}저장을 최우선(이메일 실패가 저장을 막지 못하게 분리,emailStatus로 UI에 결과 전달).static-pages/feedback.html신규 →/feedback로 배포. 유형 드롭다운·이름·회신 이메일(선택)·메시지,pageUrl: document.referrer로 어느 페이지에서 보냈는지 기록.- 진입점 2곳: 루트 사이드바
✉️ 피드백버튼(rebuild-site.ps1이 398개 페이지에 주입,data-router-ignore로 §5의 SPA 라우터 우회) + 질문하기 페이지(ask.html) 하단 피드백 링크. - 프라이버시:
50. Site Feedback/는 robocopy 미러 단계에서/XD로 제외 → 방문자 피드백(민감할 수 있는 개인 연락처·비판)은 사이트에 게시되지 않고 David의 로컬 옵시디언·이메일에서만 확인.type: site-feedback신규 운영 타입. - 동기화:
sync-queries.ps1(10분)·rebuild-site.ps1(1시간) 양쪽에feedback:처리 블록 추가(Q&A 동기화와 같은 dedup·파일명 슬러그 패턴, 처리 후 KV 키 삭제).
이메일 경로 전환 — Gmail API(도메인 전체 위임) → Resend: 처음엔 기존 Google 서비스 계정을 재사용해 Gmail API로 david 앞으로 발송하려 했으나(§6 Sheets 조회와 같은 서비스 계정), Workspace 사용자 “대신(as)” 보내려면 도메인 전체 위임이 필요하고 이는 Google Workspace 관리자 콘솔에서만 승인 가능한데 사용자가 관리자 계정 접근이 없어 막힘. → 관리자 권한이 전혀 필요 없는 Resend(resend.com)로 전환: turboairbrain.uk가 이미 Cloudflare DNS라 도메인 인증(SPF/DKIM)이 Resend Auto configure로 자동 처리됨. 코드도 JWT/OAuth(_gmail.js, 삭제)에서 API 키 1개짜리 단순 fetch(_email.js)로 축소. 발신 feedback@turboairbrain.uk, 수신 david@turboairinc.com.au, RESEND_API_KEY는 Cloudflare Pages secret.
검증: 재배포 후 Resend API로 직접 테스트 메일 발송 → {"id":"..."} 정상 반환(도메인 인증·발송 경로 확인). Functions 번들 정상 컴파일·배포(exit 0). KV 저장+옵시디언 동기화 경로는 §1과 동일 구조라 신뢰도 높음.
재사용 가치:
- “서비스 계정 재사용”이 항상 최선은 아니다 — 같은 Google 서비스 계정이라도 Sheets 읽기(위임 불필요,
sub없음)와 Gmail 사용자 대신 보내기(도메인 전체 위임 필수)는 권한 요구가 완전히 다르다. 관리자 콘솔 접근이 없으면 후자는 코드가 아무리 맞아도unauthorized_client로 막힌다. 관리자 권한이 없는 환경에선 제3자 이메일 API(Resend 등) + 이미 보유한 도메인의 DNS 인증이 훨씬 짧은 경로. - KV-우선 + 이메일 best-effort 분리 — 알림 채널(이메일)이 실패해도 메시지 자체는 절대 유실되지 않는 구조(KV → 옵시디언). §1의 always-200 + source-of-truth 패턴이 Q&A뿐 아니라 폼 제출 전반에 그대로 재사용됨.
- 게시 제외(
/XD)로 프라이버시 경계 만들기 — 같은 볼트 안에 있어도 특정 폴더를 미러 단계에서 빼면 “로컬 전용” 영역을 만들 수 있다. 방문자 피드백처럼 공개돼선 안 되는 수신함에 재사용 가능한 패턴.
후속 개선 15 (2026-08-12) — “동기화 버튼 눌러도 반영 안 됨” 재발: npx wrangler가 이번엔 실패가 아니라 hang → 로컬 바이너리로 근본 수정
요구: “TAB Wiki 지금 동기화 를 실행하였는데 사이트에는 반영이 되지 않고 있어.”
진단(후속 개선 13의 교훈 그대로 — “작업 실행됨 ≠ 빌드 성공”):
rebuild.log확인 → 13:56 마지막 성공 이후 14:46·15:46·15:59 재빌드가 “시작”만 찍히고 완료 라인이 전혀 없음(Build exit code조차 없음) = 빌드 이전 단계에서 멈춤.Get-ScheduledTaskInfo→LastTaskResult: 267014(=SCHED_S_TASK_TERMINATED, 작업이 완료 못 하고 10분ExecutionTimeLimit에 걸려 강제종료).Get-Process node→ 08:48부터 매 재빌드마다 생성된 orphan node 프로세스가 11개 누적. 이들은npx wrangler호출이 끝나지 않고 매달린 것.- 대조 검증: 로컬 고정 바이너리
node_modules\.bin\wrangler.cmd로 직접 KV 조회 → 즉시 정상. 즉 hang의 원인은npx wrangler의 버전 재resolve→fetch/install 단계(package.json의^4.119.0이 4.121.0 설치를 허용 → npx가 네트워크에서 받으려다 멈춤). 후속 개선 12에서 “간헐적 실패”로 나타났던 같은 뿌리가 이번엔 “무한 대기”로 발현.
해결:
rebuild-site.ps1·sync-queries.ps1의 모든& npx wrangler ...(총 14곳)를 상단에 정의한$wrangler = <절대경로>\node_modules\.bin\wrangler.cmd직접 호출로 교체 — npx 버전 resolve/fetch 자체를 제거.- 빌드 호출
& npx quartz build는 quartz가 이 프로젝트 자신의 패키지(node_modules에 shim 없음,npx quartz가 로컬 bin으로 풀려 설치 시도는 안 하지만 npx 의존을 없애기 위해) →& node .\quartz\bootstrap-cli.mjs build로 직접 호출. - orphan node 프로세스 11개 종료 후, 재빌드를 포그라운드로 실행해 63초 만에 Build 0 / Deploy 0 완주 확인 → 밀려 있던 매출·수주·TA QLD 정정 변경 일괄 게시.
함정 기록 (Edit 도구 다중 치환 시 trailing-space): & npx wrangler → & $wrangler(꼬리 공백 없이) 치환을 하니 & $wranglerkv key ...로 토큰이 붙어버렸고, 이어 kv key list가 kv keylist로 다시 붙는 실수를 반복함. 교훈: 공백을 포함한 문자열을 replace 할 때는 치환 후 반드시 grep으로 토큰 경계를 재확인하고, 앞뒤 공백을 보존하도록 매칭/치환 문자열의 공백을 대칭으로 맞출 것.
재사용 가치:
npx <tool>은 예약 작업 같은 무인 실행에 부적합 — 버전 범위가 열려 있으면 언제든 fetch/install을 시도하고, 네트워크가 느리거나 프롬프트가 뜨면 무한 대기한다. 무인 스크립트는 항상node_modules\.bin의 고정 바이너리(또는node <cli.mjs>)를 직접 부른다.- “완료 라인 부재”는 실패(exit 1)와 다른 신호 — exit 1이면
Build exit code: 1이 찍히지만, hang이면 그 라인 자체가 없다. 로그에 “시작만 있고 끝이 없다”면 특정 단계에서 프로세스가 매달린 것으로 보고Get-Process·LastTaskResult(267014=terminated)를 함께 본다.
후속 개선 16 (2026-08-12) — 사이트 렌더링에서 $가 깨지는 문제: Quartz LaTeX 플러그인 비활성화
요구: “옵시디언 문서가 사이트로 렌더링될 때 * 기호가 그대로 보인다”(스크린샷: $20,000 … $2025-26회계연도 … 구간이 공백 없이 붙고 **굵게**가 깨짐).
원인: Quartz의 @quartz-community/latex 플러그인(KaTeX)이 $…$를 인라인 수식 구분자로 해석. 이 볼트는 통화 표기에 $를 광범위하게 쓰는데($20,000, $4,610 … $ 포함 파일 61개), 두 개의 $가 수식 쌍으로 묶여 그 사이 텍스트가 수식 모드로 렌더(공백 제거·** 리터럴 노출)됐다.
해결: quartz.config.default.yaml의 @quartz-community/latex 플러그인을 enabled: false로 비활성화. 이 볼트엔 실제 LaTeX 수식을 쓰는 문서가 없어(business 위키), 끄면 모든 $가 리터럴로 렌더된다. 재빌드 후 산출물 검증: 답변 페이지 katex 렌더 0건, $20,000 리터럴 표시 확인.
재사용 가치: 통화·가격을 다루는 위키를 Quartz로 발행할 때 LaTeX/math 플러그인은 기본적으로 꺼야 한다 — $가 수식 구분자와 충돌한다. 개별 \$ 이스케이프는 파일마다 반복되므로, 실제 수식이 필요 없으면 플러그인 자체를 끄는 전역 해결이 낫다. (수식이 필요한 문서가 생기면 enabled: true로 되돌리고 통화는 \$로 이스케이프.)
Agent Capture Notes
Topic Summary
turboairbrain.uk/ask 페이지를 만들어 사장님·Nathan 매니저가 브라우저에서 질문하면 Claude API가 컴파일된 TAB Wiki 전체(약 4.3만 토큰, RAG 없이 통째로 컨텍스트에 주입)를 근거로 답하고, 그 문답을 Cloudflare KV에 기록했다가 10분 주기(전체 재빌드와 분리된 경량 작업)로 30. Queries/에 markdown Query Result로 동기화 — 1시간 주기 전체 재빌드에서 위키 컨텍스트 번들에도 편입되어 계속 누적되는 구조. 같은 날과 다음 날에 걸쳐 답변 렌더링(marked.js)·PDF 다운로드·토큰당 비용 표시·SPA 네비게이션 버그(data-router-ignore)·즉시 로컬 동기화·콜아웃 줄바꿈·답변 잘림(max_tokens/thinking) 수정·두 예약 작업의 주기 재조정 및 완전 백그라운드화까지 8차례, 매출/판매실적 질문에 Google Sheets 원본을 서비스 계정 OAuth2로 직접 라이브 조회하는 기능(9번째), Power BI 판매실적 대시보드를 공개 링크 대신 사이트 안(Basic Auth 뒤)에 임베드한 Sales Dashboard 페이지(10번째), 그리고 1시간 예약 주기를 기다리지 않고 바탕화면 더블클릭 파일로 즉시 동기화를 트리거하는 기능(11번째)까지 이어서 진행했다. 구축 과정 전체에서 Windows PowerShell 5.1의 인코딩·파이프·시크릿·경로 와일드카드 관련 버그, 그 정반대 방향인 VBScript BOM 버그, Cloudflare Pages 시크릿의 소급 미적용 버그(Google 시크릿에서 재현), 그리고 “웹에 게시” 링크를 iframe으로 감싸는 것이 실질적 보안이 아니라는 함정까지 다수 발견하고 해결(또는 사용자에게 명시적으로 트레이드오프를 안내)했다.
Key Claims By Platform
- Claude Code: 전체 인프라(KV, Pages Function, 정적 페이지, 재빌드 스크립트 확장) 구축 + 최초 10가지 + 후속 9가지(VBScript BOM 버그 포함) 트러블슈팅 항목 해결 + Google Sheets 서비스 계정 라이브 조회 통합(9번째 후속 개선, Cloudflare Pages 시크릿 소급 미적용 버그 재확인 포함) + Power BI Sales Dashboard 페이지 추가 및 사이드바 UX 조정(10번째 후속 개선, “웹에 게시” 링크의 보안 한계를 사용자에게 명시적으로 고지 후 임시 조치를 선택받음).
Evidence Gaps
- 질문당 API 비용 표시 기능은 이제 구현됐지만(후속 개선 2), KV 누적값은 “이 도구가 계산한 값”일 뿐 Anthropic 계좌의 실제 청구액이 아님 — 몇 주 사용 후 Anthropic Console 실제 청구액과 대조해 오차 범위 확인 필요.
- Prompt caching 미적용 — 반복 질문 시 매번 4.3만 토큰 전체를 새로 계산해서 과금됨. 사용량이 늘면 캐싱 도입 검토.
- “참고 문서”/“Referenced Pages” 목록이 실제
[[wikilink]]형식이 아니라 평문 — Obsidian 그래프에 자동 연결되지 않음. 향후 개선 여지. - Cloudflare 엣지가 5xx를 자체 에러 페이지로 덮어쓰는 정확한 조건(모든 5xx인지, 특정 상태코드만인지)은 확인하지 못함 — 이번엔 항상 200 반환으로 우회했을 뿐, 근본 원인은 미상.
- USD→AUD 환율은 frankfurter.app 12시간 캐시 + 고정 fallback(1.55)이라 정확한 실시간 환율은 아님 — 큰 의사결정용 숫자는 아니고 참고용.
TAB-Wiki-QuerySync(10분 폴링)는 “즉시 push”가 아니라 “최대 10분 지연 폴링”이라는 근본적 한계가 있음 — 사용자가 이 트레이드오프를 인지하고 승인함(진짜 push는 Cloudflare 유료 플랜 + 상시 로컬 프로세스 필요).max_tokens: 50000이 실제로 그만큼 긴 답변을 생성할 경우 Cloudflare Pages Function/브라우저 fetch 타임아웃에 걸릴 수 있는지는 아직 실측되지 않음(현재까지 관측된 답변은 3~4천 토큰 선).- (후속 개선 9) Google Sheets 라이브 조회는 매 요청마다 16,800행+ 시트를 전량 fetch+집계한다 — 짧은 시간에 매출 질문이 연속으로 들어올 경우 Google Sheets API 자체 rate limit(할당량)에 걸릴 가능성은 아직 실측되지 않음. OAuth access token은 KV에 캐시되지만, 시트 데이터 자체는 매 요청마다 새로 fetch(요구사항상 “항상 최신”이 원칙이라 의도된 설계이지만, 사용량이 늘면 캐시-무효화 전략이 필요할 수 있음).
- (후속 개선 9) TA QLD 식별이 Company 필드의 “Queensland”/“QLD” 문자열 매치라는 이름 기반 추정 방식은 영업판매실적 조회 절차가 이미 명시한 한계를 그대로 물려받은 것 — 확정된 컬럼 매핑으로 바뀌지 않는 한 라이브 조회에도 동일하게 남아있는 가정.
- (후속 개선 10) Sales Dashboard 페이지는 Power BI “웹에 게시” 공개 링크를 그대로 쓰는 임시 조치 — 원본 URL 자체가 완전 공개 상태라는 근본 문제는 해소되지 않았다. 언제 Azure AD 앱 등록 + 서비스 주체 임베드 토큰 방식(진짜 보안)으로 전환할지, Power BI 라이선스(Pro 이상) 필요 여부는 미결정.
Suggested Wiki Targets
→ Cloudflare Pages Function + Claude API로 Wiki 기반 Q&A 봇 만들기로 컴파일 완료 (2026-08-04: 후속 개선 6·7 내용 §2·§4에 추가 반영, 2026-08-05: 후속 개선 9 내용 §6 신설, 후속 개선 10 내용 §7 신설, 후속 개선 11 내용 §4에 추가 반영).20. Wiki/23. Guides/— “Cloudflare Pages Function + Claude API로 Wiki 기반 Q&A 봇 만들기” 가이드→ Windows PowerShell 5.1 CLI 트러블슈팅 패턴 모음으로 컴파일 완료 (PowerShell CLI 파이프의 BOM 오염 문제와 상호 링크).20. Wiki/21. Concepts/— “Windows PowerShell 5.1 인코딩·경로 함정 모음”→ 별도 페이지 대신 Cloudflare Pages Function + Claude API로 Wiki 기반 Q&A 봇 만들기 §5에 흡수 (범위가 작아 독립 페이지보다 가이드 내 서브섹션이 적절하다고 판단).20. Wiki/21. Concepts/— “Quartz SPA 라우터와 커스텀 정적 페이지 충돌”
다음에 할 일 (Next Steps)
- 몇 주 사용 후 Anthropic Console 실제 청구액과 KV 누적 사용액(자체 추정치)을 대조해 오차 확인.
- “참고 문서” 목록을 실제
[[wikilink]]로 만들어 Obsidian 그래프에 연결되도록 개선(문자열 매칭 필요, 후순위). functions/api/ask.js의 질문 길이 제한(2000자)·rate limit 등 안전장치가 충분한지 재검토(현재는 Basic Auth만으로 접근 제어).- 사장님·Nathan 매니저에게
/ask페이지 사용법 안내(어디서 접속하는지, 얼마나 걸리는지, PDF 저장 방법 등). - Prompt caching 도입 검토(사용량 증가 시).
max_tokens: 50000로 실제 매우 긴 답변이 나올 경우 Cloudflare Pages Function/브라우저 fetch 타임아웃에 걸리는지 실측 확인(후속 개선 6).- Google Sheets 라이브 조회의 실제 응답 지연(fetch+집계에 걸리는 시간)을 실측해 사용자 체감 대기시간에 문제가 없는지 확인(후속 개선 9).
- Google Sheets 서비스 계정 API 호출량이 늘어날 경우 Google Cloud 쿼터 대시보드를 주기적으로 확인.
- Sales Dashboard(후속 개선 10)를 Power BI Embedded(Azure AD 앱 + 서비스 주체 토큰) 방식으로 전환할지 결정 — 전환 시 필요한 Power BI 라이선스 등급 확인.
sync-now.vbs(후속 개선 11)를 바탕화면 외에 시작 메뉴/작업 표시줄 고정 등 더 접근성 높은 위치에도 둘지 검토(현재는 바탕화면 파일 1곳).- 2026-08-05 밤~06 새벽 사이
TAB-Wiki-QuerySync가 왜 아예 실행 로그조차 없었는지(랩탑 절전/종료 추정, 미확정) 원인 확정 — 랩탑이 상시 켜져있지 않은 구조라면 예약 작업 기반 동기화 자체의 한계로 별도 논의 필요. wrangler외에npx로 호출하는 다른 CLI 도구가 이 프로젝트에 더 있는지 점검해 동일하게package.json에 고정할지 확인(후속 개선 12).
Ingest Notes
- Category: AI Research
- Compiled into (2026-08-03 ingest): Cloudflare Pages Function + Claude API로 Wiki 기반 Q&A 봇 만들기 (Guide, 신규) · Windows PowerShell 5.1 CLI 트러블슈팅 패턴 모음 (Concept, 신규) · Obsidian 볼트를 Cloudflare Pages로 웹 발행하기 (§6 확장 섹션 추가) · PowerShell CLI 파이프의 BOM 오염 문제 (상호 링크 추가) · MOC-TAB 프로젝트 (인프라·기술 섹션 갱신).
- 갱신 (2026-08-04, 후속 개선 6·7·8 반영): Cloudflare Pages Function + Claude API로 Wiki 기반 Q&A 봇 만들기 §2(max_tokens/adaptive thinking 함정) · §4(VBScript BOM 함정, 예약 작업 주기 표) 추가 갱신.
- 갱신 (2026-08-05, 후속 개선 9 반영): Cloudflare Pages Function + Claude API로 Wiki 기반 Q&A 봇 만들기 §6(Google Sheets 서비스 계정 라이브 조회 아키텍처, Cloudflare Pages 시크릿 소급 미적용 함정) 신설 · 영업판매실적 조회 절차 (수동 Google Drive 커넥터 경로와 별개로,
/ask가 자동으로 사용하는 서비스 계정 라이브 조회 경로 추가) 갱신. - 갱신 (2026-08-05, 후속 개선 10 반영): Cloudflare Pages Function + Claude API로 Wiki 기반 Q&A 봇 만들기 §7(Ask 이외 커스텀 정적 페이지 추가 레시피, “웹에 게시” 링크 iframe 임베드의 보안 한계, Quartz 사이드바
gap오버라이드, alias ASCII 슬러그를 이용한 Wiki 백링크 패턴) 신설. - 갱신 (2026-08-05, 후속 개선 11 반영): Cloudflare Pages Function + Claude API로 Wiki 기반 Q&A 봇 만들기 §4(기존 예약 작업을
schtasks /run으로 재사용하는 수동 즉시-트리거 패턴, VBScriptMsgBox시스템 로케일 함정과.vbs/.ps1책임분리 해결) 추가 갱신. - 갱신 (2026-08-06, 후속 개선 12 반영): Cloudflare Pages Function + Claude API로 Wiki 기반 Q&A 봇 만들기 §4(
npx wrangler미고정으로 인한 간헐적 동기화 실패와package.jsondevDependency 고정 해결) 추가 갱신. - 갱신 (2026-08-12, 후속 개선 14 반영): Cloudflare Pages Function + Claude API로 Wiki 기반 Q&A 봇 만들기 §8(방문자 피드백 폼 → KV → 이메일+로컬 옵시디언 파이프라인, 관리자 권한 없이 Gmail 도메인 전체 위임 회피하고 Resend로 전환, 게시 제외
/XD로 프라이버시 경계 만들기) 신설 ·50. Site Feedback/README.md(운영자 전용 수신함 설명) 신규.