← 목록으로

오픈 준비 작업 보고 — 8/22~8/23

계획서(open-prep)의 10일치를 이틀에 돌렸습니다. 커밋 13개.
그런데 계획에 없던 게 더 컸습니다 — 고친 것의 절반이 "코드는 있는데 조용히
죽어 있던 것"이었습니다.


0. 한 줄

이 서비스는 알림도, 웹 푸시도, 요약도, 기억도 전부 코드가 있는 채로 안 돌고 있었습니다.
전부 에러를 삼키고 있어서 아무도 몰랐습니다. 지금은 다 돕니다.


1. 계획표 진행

무엇 상태
1일차 복잡도 라우터 제거
2일차 알림 배선 복구 + 웹 푸시 가드
추가 .env 웹 번들 노출 차단
3일차 선발화 스케줄 티커
4일차 keep-alive 심박
6~7일차 도달 3층 ✅ (이메일 키만 대기)
추가 웹 푸시(크롬)
8~9일차 한국어 로컬 검색 + 지어내기 차단
10일차 다자방 턴 규칙
추가 프롬프트 다이어트 · 모델 통일 · 기억 원장

2. 조용히 죽어 있던 것들 (이번에 다 깨웠습니다)

이게 이번 작업의 진짜 내용입니다. 하나하나가 "있다고 믿었는데 없던 것"입니다.

무엇 어떻게 죽어 있었나 어떻게 알았나
알림 발송 FCM 발송기가 구 함수에만 있고 앱은 신 함수를 호출 → 배포 이래 0건 notifications 0행
웹 푸시 서비스워커·웹앱·VAPID 키가 하나도 없었음 웹에서 알림 0건
요약 fal 키 401 → 조용히 실패 상담 906건 중 요약 18건
기억 추출 같은 fal 키 401 원장이 안 쌓임
상담 조회 없는 컬럼(team_id)을 select → 쿼리 통째 거절 userId·summary가 전부 undefined
1:1 기억 주입 코드가 0줄 프롬프트 계측
1:N 기억 주입 RPC·테이블이 없어 항상 빈손 DB 조회
복잡도 라우터 헤비를 0% 포착, 87%를 헤비로 밀어올림 실사용 145건 재생
fast path fal 401 + try/catch 없음 → "안녕"이 500 실호출

관통하는 원인 하나: 실패를 삼키고 있었습니다.
catch {}, 에러 무시, 조용한 폴백. 그래서 "동작한다고 믿는 상태"가 몇 달 유지됐습니다.
이번에 고친 코드는 전부 실패하면 소리를 내게 바꿨습니다.


3. 새로 생긴 것

선발화 — 근거 없는 알림은 DB가 막습니다

reminders.evidence_utteranceNOT NULL입니다. 사용자가 자기 입으로 말한 원문이
없으면 리마인드 행이 아예 안 생깁니다. "약속한 적 없는 알림은 잔소리라 즉사"를
프롬프트가 아니라 스키마가 강제합니다 — 프롬프트는 흘리지만 CHECK는 안 흘립니다.

pg_cron 1분 잡 하나가 때가 된 것만 집습니다. 상태는 pending → firing → sent/failed로,
보내지도 않고 sent로 적는 자리를 안 만들었습니다.

심박 — 확인만 하지 않고 되살립니다

Supabase 무료 플랜은 1주 무활동이면 멈추고 pg_cron도 같이 죽습니다(자동 재개 없음).
클라우드플레어 워커가 하루 한 번 깨우고, 잡이 없거나 꺼져 있으면 그 자리에서 다시 겁니다.
등록 여부가 아니라 실제로 돌았는지를 봅니다.

실측으로 잡을 통째로 지워보고 되살아나는 것까지 확인했습니다.

웹 푸시 — 탭을 닫아도 옵니다

Firebase 웹 앱을 새로 만들고(기존엔 twitter_clone이라는 엉뚱한 이름뿐), VAPID 키를
발급하고, 서비스워커를 붙였습니다. 알림센터 도달 확인.

배우면서 알게 된 것: 탭이 열려 있으면 비활성이어도 메시지는 서비스워커가 아니라
페이지로 갑니다.
서비스워커 배경 핸들러는 페이지가 하나도 없을 때만 돕니다.
그래서 "배경 탭에 띄워둔 채 다른 일 하는" 가장 흔한 상황에 아무것도 안 보였습니다.

검색 — 지어내면 못 나갑니다

카카오 로컬(무료) 연결. 그리고 크루가 답에 쓴 상호를 [출처]로 인용하게 하고,
그걸 검색 결과와 결정론으로 대조합니다. 틀리면 재생성하고, 그래도 안 되면
정직하게 말하되 찾은 게 있으면 그 목록은 그대로 줍니다(내용 0으로 안 만듦).

검색 0건인 질문
기장 일광 국숫집 '일광당'을 주소·가격까지 지어냄 "정확히 일치하는 정보가 없네요" + 상호 0개

다자방 — 전원 발언 강제를 버렸습니다

LLM은 "다음에 누가 말해야 하나"를 못 맞춥니다(다음 화자 예측 46%, 랜덤보다 낮음).
그래서 순서를 계산시키지 않고, 사람의 턴테이킹 규칙을 씁니다 — 현재 화자가 다음 화자를
지목
하고 아무도 지목 안 하면 끝.

실측: 복합 질문은 3턴, "고마워 잘 볼게"2턴 만에 자발적 종료.


4. 기억 — 형 지시로 가장 크게 바뀐 곳

팀 기억 원장을 새로 만들었습니다

기억이 크루별로만 있어서 크루를 바꾸면 나를 다시 몰랐습니다.
설계가 말한 "비서가 나가도 회사 문서는 남는다"의 정반대였습니다.

user_memory_entries가 스키마로 두 가지를 강제합니다:

실측 — 크루를 바꿔도 압니다

부산로컬가이드에게 "8살 딸 하윤이랑 다녀 / 시끄러운 데 질색 / 주차되는 데만 /
다음 달 부산 이사"
→ 원장에 4건 기록.

한 번도 대화한 적 없는 가계부여왕에게 "이번 달 예산 짤 건데":

"이번 달 예산은 다음 달 부산 이사비까지 포함해서 짜야 해요."
"하윤이와 함께 이동하고 주차 가능한 조용한 곳을 찾는 조건이면…"

설명을 한 글자도 다시 안 했는데 압니다.


5. 프롬프트 다이어트 — 재고 나서 잘랐습니다

먼저 계측을 넣고 실측했습니다. 6턴에 5,172자였고 히스토리가 3,353자(65%)였습니다.
규칙과 기억이 그 뒤에 묻히는 구조였고, 검색 결과를 넣어줘도 크루가 지어낸 게
이것 때문
이었습니다.

전(8턴)
히스토리 2,948자 1,929자 (−35%)
총 프롬프트 4,759자 3,740자 (−21%)

두 가지를 했습니다 — 최근 6개만 원문(이전은 한 줄 요약), 히스토리 안의 발화도
300자에서 자름.
크루 답변 하나가 900~1,000자라 6개만 담아도 3,000자가 됩니다.
맥락 잇는 데 답변 전문은 필요 없습니다. 이제 ~2,200자에서 평평해집니다.


6. 모델 — 하나로 통일했습니다

같은 프롬프트를 모델만 바꿔 재봤습니다.

모델 턴당 지어내기 방지 지목 규칙
grok-4.1-fast-non-reasoning 1.12원
gemini-3.1-flash-lite 1.89원
gpt-5.6-luna 1.51원
gpt-5-mini 2.17원 ⚠️ 빈 응답 ⚠️
gemini-3.6-flash 5.25원 ⚠️ 위반 ⚠️

비싼 게 나은 게 아니었습니다. 4배 비싼 모델이 규칙을 어겼습니다.
5.6 계열에서 luna가 제일 쌉니다(sol·terra는 10배). 컨텍스트 105만.

203개 크루 전부 gpt-5.6-luna로 통일. 개성은 모델이 아니라 프롬프트가 만듭니다.
앞으로 이걸 계속 재도록 모델 비교 경로를 넣어뒀습니다.


7. 남은 것

바로 할 수 있는 것
- 채팅 표현 트랙 6건 — 크루 발화가 900~1,000자라 말풍선 문법과 안 맞습니다
- 설계서 갱신 — 기억 층은 문서와 실제가 많이 달랐습니다

형이 정해야 하는 것
- 위임(헤비 미션 분해)을 1호에 넣을 것인가. 지금 앱은 위임을 호출하는데 엣지 함수가
안 받아서 조용히 아무 일도 안 일어납니다. 2호로 미룬다면 그 호출을 없애거나
정직한 안내를 돌려줘야 합니다.
- 이메일 공급자 — 아이폰·웹 사용자에게 닿는 마지막 층. Resend 무료(가입 필요) 또는
Cloudflare(Workers 유료 $5/월). 층은 이미 짜여 있고 키만 넣으면 삽니다.

아직 못 하는 것 (정직하게)
- 아이폰 사파리는 홈 화면에 추가해야만 알림 권한을 줍니다. 그 전까지는 인앱에만 남습니다.
- 네이버 검색 키가 없어 블로그·카페 글은 못 가져옵니다(장소는 카카오로 됩니다).
- 상담 요약은 이제 돌지만, 쌓인 906건은 여전히 대부분 비어 있습니다.