Files
wandergo/docs/design/ai-course.md
T
ttipo 29e9611bd9 docs: AI 코스 생성 설계안 추가
MVP 의 AI 코스 생성(디자인 화면 8–10)을 만들기 전에 흐름·API·비용 보호·평가 방법을 정한다.
DB 가 후보를 고르고 AI 는 후보 번호로만 고르며, 이동 시간·영업·반려 조건 검증과 순서 고치기는 코드가 한다.
화면의 사실 값(영업시간·동반 조건·출처)은 모두 DB 에서 붙여 AI 가 지어낼 수 없게 한다.

§13 의 8가지(하루 횟수·비용 상한·영업시간 미확인·반려 엄격도·이동 시간·공연 제외·보관 기간·모델)는
2026-11-05 사용자가 추천안대로 정했다. 대구 평가셋을 만들며 찾은 것도 반영했다 — 조건 해석 issues 3개,
별칭이 가리키는 장소를 중심으로, 전시는 실내, 실내 후보가 모자라면 알림으로, 코스를 못 만들면 이유를 함께.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 23:23:16 +09:00

36 KiB
Raw Blame History

AI 코스 생성 설계 (v0.2, 2026-11-05)

사용자가 쓴 한 문장과 조건으로 하루 코스를 만들어 보여 주는 기능(디자인 화면 8–10: AI 조건 입력 · 만드는 중 · 결과)을 어떻게 만들지 정한다. 아직 코드는 없다. 장소·축제 데이터는 장소 DB 설계를 그대로 쓰고, 평가는 docs/ai-eval/ 를 쓴다. §13 의 8가지는 2026-11-05 사용자가 추천안대로 정했다. 구현은 §14 순서로 한다.

v0.1 → v0.2: §13 결정을 반영했다. 평가셋 대구판을 만들며 찾은 것도 고쳤다. 조건 해석 issues 3개 추가(§2), 별칭이 가리키는 장소를 중심으로(§2), 전시는 실내(§3.3), 비 올 때 실내 비율은 후보가 모자라면 알림으로(§3.3·§3.6), 코스를 못 만들면 이유를 함께(§3.5·§3.6·§4).

0. 전제 (이미 정한 것)

전제 근거
AI 후보는 자체 장소 DB(place_listing)와 festival_event 에서만 고른다. 카카오 응답은 LLM 에 보내지 않는다 장소 DB §0, docs/research/pre-dev-check/kakao_place_id.md
후보 순위: 운영자가 추천·확인한 곳(1순위) → 관광공사 장소(2순위). 확인 안 된 상가정보 매장은 쓰지 않는다 장소 DB §3 ai_tier
DB 가 후보를 고르고, AI 는 ID 로만 선택하고, 코드가 검증하고 순서를 맞춘다 기획 검토 보고서(2026-10-28)
횟수: 비로그인 하루 1~2회, 로그인하면 늘린다. 처음 만들기·다시 만들기만 차감하고 후보로 바꾸기는 차감하지 않는다 MVP 결정(2026-10-28·29)
결과에 고른 이유와 출처를 보여 준다. 바꾸기 후보는 생성 때 함께 받는다(추가 AI 호출 없음) MVP 결정(2026-10-29), 디자인 결과 화면
AI 가 만든 결과임을 표시한다 기획 검토 보고서(AI 기본법 생성물 표시)
영업시간이 없는 곳은 "영업시간 미확인 · 방문 전 확인"으로 표시하고 AI 시간 검증에서 뺀다 착수 전 확인(2026-10-29)
만들기·보기는 비로그인으로 되고, 저장은 로그인해야 한다 MVP 결정
한국어·영어로 출시하고 4개 언어를 전제로 설계한다 MVP 결정
기본 모델은 평가셋으로 정한다 기획 검토 보고서 미결 항목

1. 한눈에 보기

[화면 8 입력]                          [화면 9 만드는 중]                    [화면 10 결과]
 한 문장 ─▶ 조건 해석 ─▶ 조건 확인·수정 ─▶ 생성 시작 ─▶ 진행 조회(1~2초마다) ─▶ 결과
            (AI 1회, 차감 없음)             (차감 대상)                          │
                                                       후보로 바꾸기(앱 안) · 평가 · 다시 만들기 · 저장(로그인)

생성 작업 (서버 백그라운드)
 ① 후보 찾기   반경 안 1·2순위 장소 + 그날 행사               → "수성못 근처에서 128곳을 찾았어요"
 ② 거르기      영업·반려동물·공휴일·행사 시간 (코드)           → "토요일 영업·반려견 동반으로 31곳을 추렸어요"
 ③ 코스 짜기   후보 40곳 이하 → AI: 고르기·순서·머무는 시간·이유
               → 코드: 이동 시간·시간표·검증·고치기             → "날씨와 동선에 맞춰 시간표를 짜는 중"
 ④ 마무리     사실 값 붙이기 · 바꾸기 후보 검증 · 출처          → "고른 이유를 정리할게요"

원칙은 세 가지다.

  1. 사실은 DB 가, 말은 AI 가. 영업시간·반려동물 조건·행사 기간·출처처럼 화면에 나오는 사실 값은 모두 DB 에서 붙인다. AI 는 장소를 고르고 한 줄 이유와 요약만 쓴다.
  2. AI 는 후보 목록의 번호로만 고른다. 목록 밖 장소나 지어낸 장소는 검증에서 버린다(구조화 출력 + 코드 검증).
  3. 확인할 수 없는 것을 확인된 것처럼 보이지 않는다. 미확인 값은 '미확인'으로 표시한다. 위반을 고칠 수 없으면 장소 수를 줄이고 이유를 알린다.
일 맡는 쪽
문장에서 지역·날짜·시간 같은 조건 뽑기 AI(작은 모델). 사용자가 확인·수정한다
후보 찾기·거르기 코드(SQL)
어떤 곳을, 어떤 순서로, 몇 분씩 AI
이동 시간·도착 시각·영업 여부·반려 조건 검증 코드
순서 고치기·머무는 시간 조정 코드(장소가 6곳 이하라 모든 순서를 따져 볼 수 있다)
이유·요약 문장 AI
화면의 사실 값·출처 코드(DB)

2. 조건 해석 (입력 화면)

사용자가 문장을 다 쓰면(입력이 1초 멈추거나 키보드 '완료') 한 번 부른다. 글자마다 부르지 않는다. 횟수를 차감하지 않는다.

POST /api/course-conditions — 요청 { text, baseArea }, 응답 { conditions, inferred, issues, weather }

조건(CourseConditions)은 생성 요청에도 그대로 쓴다.

항목 값 비었을 때 기본값
area {type:'neighborhood', key}(downtown · suseongmot · apsan) · {type:'point', lat, lng}(현재 위치) · {type:'place', placeId}("서문시장 근처") 앱의 기준 위치(baseArea)
date YYYY-MM-DD, 오늘 ~ 30일 뒤 오늘(18시 이후면 내일)
start · end HH:mm, 2~12시간, 자정을 넘기지 않는다(MVP) 오늘이면 다음 정시, 아니면 13:00 · 시작 + 6시간(22시까지)
party 1~10 2
pet {kind: dog·cat·other, size: small·medium·large·null, count} 또는 null null
mobility walk · transit · car walk
indoorIfRain boolean true
  • inferred 는 문장에서 뽑은 항목 목록이다. 화면의 "적은 내용에서 자동으로 채웠어요" 에 쓴다.

  • issues 는 사용자에게 되묻거나 알릴 것이다. 앱이 이유를 보여 주고 그 조건을 고치게 한다. 평가셋의 ask_clarification · decline_out_of_scope 사례는 이 단계에서 다룬다(§9).

    값 경우
    unsupported_area 대구 밖 지역("성수동", "경주 황리단길"). 지금은 대구만 된다
    past_date · date_out_of_range 지난 날짜 · 30일보다 먼 날짜("다음 달 크리스마스")
    multi_day "3박 4일", "1박" — 하루 코스만 된다
    too_short · past_midnight 2시간보다 짧다("점심시간 1시간만") · 자정을 넘긴다("밤 11시 이후")
    unclear_request 코스 요청으로 읽을 수 없다("ㅁㄴㅇㄹ")
  • 지역 문구는 AI 가 뽑고, 코드가 동네 이름·별칭 → 장소 DB 이름 검색 순서로 찾는다. 대구 영역(shared/utils/geo.ts 의 isInSido('27', …)) 밖이면 unsupported_area 다. 현재 위치가 대구 밖이면 지금 앱처럼 도심으로 대신하고 안내한다.

  • 별칭이 특정 장소를 가리키면(김광석길 → 김광석다시그리기길, 서문시장) 그 장소를 중심으로 한다({type:'place'}). 김광석다시그리기길은 도심 중심에서 1.55km 라, 동네 중심으로 잡으면 걸어서 반경(1.5km) 밖이 된다.

  • weather 는 고른 날짜·지역의 예보다(§3.3). 입력 화면의 "토요일 오후 비 60%" 에 쓴다.

  • 모델은 작은 모델(Haiku 5.5)에 구조화 출력을 쓴다. AI 호출이 실패하면 기본값만 채운 조건을 돌려준다(입력 화면을 막지 않는다).

  • AI 에는 문장, 오늘 날짜·요일, 지원 동네 목록만 보낸다. 좌표·기기 정보는 보내지 않는다.

3. 생성

3.1 단계와 진행 표시

생성은 서버의 백그라운드 작업이다. 앱은 GET /api/course-generations/{id} 를 1~2초마다 불러 단계와 숫자를 받고, 화면 문구는 앱이 언어별로 만든다.

stage 서버가 하는 일 함께 주는 값 화면 문구(예)
searching 후보 찾기 found "수성못 근처에서 128곳을 찾았어요"
filtering 거르기·줄이기 kept "토요일 영업·반려견 동반으로 31곳을 추렸어요"
planning AI 호출 → 시간표·검증·고치기 — "날씨와 동선에 맞춰 시간표를 짜는 중"
finishing 사실 값·바꾸기 후보·출처 붙이기 — "고른 이유를 정리할게요"
  • 스트리밍(SSE) 대신 조회를 쓰는 이유: 표준 응답 형식을 그대로 쓸 수 있고, 앱이 잠깐 백그라운드로 가도 같은 id 로 이어서 볼 수 있다. 앱 Dio 의 받기 제한(15초)에 걸리는 긴 요청도 없다. 생성은 1020초라 조회 1020번이면 끝난다.
  • 작업 제한 시간은 45초다(AI 호출 30초 + 재시도 여유). 넘으면 failed + AI_002.
  • MVP 는 서버가 한 대라 프로세스 안에서 돌린다. 동시 AI 호출은 5건까지이고, 넘으면 queued 로 기다린다. 서버를 늘리면 Redis 큐(BullMQ)로 옮긴다(nestjs 스킬의 Redis 확장 지점).
  • 종료할 때 진행 중인 작업은 AI 호출을 끊고 failed(차감 없음)로 남긴다(OnApplicationShutdown). 기동할 때는 2분 넘게 running 인 작업을 failed 로 바꾼다.

3.2 후보 찾기

  • 장소: place_listing 에서 is_searchable 이고 ai_tier 가 1·2 인 곳. 숙박은 순위와 관계없이 뺀다.
  • 반경: 걸어서 1.5km(장소 목록 API 기본값과 같다) · 대중교통 6km · 자동차 20km(도심에서 팔공산까지). 중심은 조건의 area 다.
  • 행사: festival_event 중 그날 진행하고 반경 안에 있으며 숨기지 않은 것. 종류별 기간 상한(장소 DB §2.4)을 적용한다. 축제·행사·전시만 넣는다. 공연(KOPIS)은 회차 시각을 아직 해석하지 않아 MVP 후보에서 뺀다(§13).

3.3 거르기 (코드)

조건 규칙
영업 그날 요일 기준으로, 시간 범위 안에서 분류별 최소 머무는 시간(§3.6)만큼 열려 있어야 한다. isOpenAt 으로 10분 간격 확인(브레이크 포함). 휴무 요일이면 뺀다
영업시간 미확인 status 가 unknown · varies 이거나 seasonal 이면 미확인이다. 자연·관광지와 행사만 남기고(야외라 시간 영향이 작다) 식당·카페·문화시설·레저는 뺀다(§13). 하절기·동절기처럼 계절별 시간표는 지금 미확인으로 읽힌다(AI 후보 23곳, 문화시설 10곳 — 수창청춘맨숀·대구예술발전소·향촌문화관 등). 달 범위가 적혀 있으면 생성 날짜로 고르도록 2단계에서 고친다
공휴일 그날이 공휴일·명절이고 holidayClosures 가 있으면 뺀다. 공휴일 표는 코드 상수로 두고 해마다 갱신한다
반려동물 (조건에 있을 때) not_allowed 와 크기 불일치(petSizes 가 있는데 그 크기가 없음)는 뺀다. 실내(식당·카페·문화시설·쇼핑과 전시)는 allowed · partial 만 남긴다. 야외 분류와 축제·행사는 unknown 도 남기고 결과에 "동반 정보 없음" 경고를 붙인다(§13). 2026-11-05 기준 대구에 동반이 확인된 실내 장소는 0곳이라, 큐레이션 전에는 반려 코스가 야외로만 나온다
행사 시간 playtime_text 를 영업시간 파서로 읽어 시간 범위와 겹쳐야 한다. 못 읽으면 남기고 "행사 시간 확인" 경고를 붙인다
다시 만들기 이전 결과의 장소는 남은 후보가 15곳 이상이면 빼고, 아니면 목록 뒤로 보낸다
비 거르지 않는다. 강수확률이 40% 이상이고 indoorIfRain 이면 AI 에 '실내 위주'를 알리고, 검증에서 실내 비율 50% 이상을 본다. 실내 후보가 모자라 채울 수 없으면 코스는 내고 notices 에 알린다
  • 실내·실외: 큐레이션 setting 을 쓰고, 없으면 분류로 추정한다(문화시설·식당·카페·쇼핑과 전시는 실내, 자연은 실외, 관광지·레저·축제·행사는 미확인).
  • 날씨: 기상청 단기예보(동네예보 격자). 오늘부터 3일 뒤까지 나오고, 17시 이후 발표분은 4일 뒤까지 나온다. 그 뒤 날짜는 weather: null 이다. 입력 화면에 "예보 없음"을 보여 주고 비 조건은 쓰지 않는다. 격자·발표 시각별로 1시간 캐시한다. 2026-10-29 점검 스크립트로 이미 불러 본 API 다(docs/research/data-check/).

3.4 후보 줄이기 (최대 40곳)

AI 에 너무 많이 주면 비용이 늘고 고르는 품질도 떨어진다.

  • 1순위(확인한 곳)를 먼저, 그다음 거리순으로 둔다.
  • 분류마다 최대 10곳이다. 한 분류(예: 관광지)가 목록을 다 채우지 않게 한다.
  • 행사는 가까운 순서로 최대 8개다.
  • AI 에는 장소 id(uuid) 대신 짧은 번호(P01P40, E01E08)를 준다. 번호와 id 의 대응은 서버만 가진다.

3.5 AI 호출

입력은 시스템 프롬프트(규칙과 출력 형식. 요청마다 같다)와 사용자 메시지다.

요청: "비 오는 토요일 오후, 강아지랑 수성못 데이트"
조건: 2026-11-07(토) 14:00–20:00 · 2명 · 소형견 1 · 걸어서(구간 최대 15분) · 실내 위주(비 60%)
후보:
P01 | 수성못 유원지 | 자연 | 실외 | 중심 0m | 상시 개방 | 반려: 가능(관광공사) | 관광공사
P02 | ○○카페 | 카페 | 실내 | 북동 420m | 10:00–22:00 | 반려: 소형 가능·테라스석만(11.02 확인) | 확인한 곳: "못이 보이는 2층 창가"
…
E01 | ○○ 빛 축제 | 축제 | 실외 | 남 300m | 18:00–22:00 | 반려: 정보 없음 | 11.06–11.08
  • 후보마다 이름(요청 언어), 분류, 실내·실외, 중심에서의 방향·거리, 그날 영업시간, 반려 조건, 출처, 확인한 곳의 추천 이유를 준다. 소개 앞 80자도 넣을지는 평가로 정한다(장소를 고르는 데 도움이 되지만 토큰이 늘어난다).
  • 좌표·사용자 위치·기기 정보는 보내지 않는다. 거리와 방향은 서버가 계산한다.

출력은 구조화 출력(JSON 스키마)으로 받는다.

필드 내용
title 코스 이름(20자 이내). 저장할 때 쓴다
summary 코스 전체 설명 한두 문장(120자 이내)
stops[] ref(P·E 번호) · minutes(머무는 시간) · reason(60자 이내)
alternates[] forStop(몇 번째 대신) · ref · reason. 장소마다 최대 1개
notices[] 조건을 다 맞추지 못한 이유("대형견 동반 실내 전시가 2곳뿐이라 2곳만 넣었어요"), 들어줄 수 없는 부분

프롬프트 규칙 (요지)

  • 후보 목록의 번호만 쓴다. 같은 곳을 두 번 쓰지 않는다.
  • 이유는 후보에 적힌 사실(분류·실내외·거리·영업시간·반려 조건·추천 이유·소개)로만 쓴다. 가격·분위기·메뉴·혼잡도처럼 데이터에 없는 내용("조용한", "저렴한" 등)은 쓰지 않는다.
  • 이유에 순서 표현("먼저", "마지막으로")을 쓰지 않는다. 코드가 순서를 바꿀 수 있기 때문이다.
  • 시간 범위가 식사 시간대(점심 11:30–14:00, 저녁 17:30–20:30)에 걸치면 식당을 그 시간에 둔다.
  • 위험하거나 범위 밖인 부탁(음주 운전, 시스템 정보 요청 등)은 그 부분만 빼고 notices 에 알린다. 코스는 조건대로 만든다.
  • 요청의 핵심(예: '카페 투어', '실내 전시만')을 맞출 후보가 없으면 다른 곳으로 채우지 않는다. stops 를 비우고 notices 에 이유를 쓴다(→ AI_003, §3.6). 일부만 못 맞추면("산책하고 카페도") 맞출 수 있는 만큼 만들고 알린다.
  • 요청 언어(한국어·영어)로 쓴다.

호출 설정

  • 모델과 effort 는 평가로 정한다(§8). 추론(adaptive thinking)은 effort 를 낮춰 지연을 줄이는 쪽부터 시험한다.
  • 시스템 프롬프트는 프롬프트 캐시에 올린다. 트래픽이 적으면 캐시가 5분 안에 다시 쓰이지 않아 이득이 작다.
  • 제한 시간 30초, SDK 재시도 1회. 프롬프트는 코드에 두고 버전(PROMPT_VERSION)을 생성 기록에 남긴다.

3.6 시간표·검증·고치기 (코드)

이동 시간 추정 — MVP 는 경로 API 없이 직선거리로 계산한다.

이동 계산 구간 최대
걸어서 직선거리 × 1.3 ÷ 분당 70m 15분
대중교통 10분(걷기·대기) + 직선거리 × 1.4 ÷ 분당 300m 30분
자동차 5분(주차) + 직선거리 × 1.4 ÷ 분당 400m 30분
  • 화면에는 "약 N분"으로 보이고 추정값임을 표시한다. 실제 경로 API(대중교통 ODsay 등)는 2단계다(§12).
  • 계수와 속도는 상수로 둔다. 큐레이션 동네에서 실제로 걸어 본 시간과 비교해 맞춘다.

머무는 시간 범위 — AI 가 정한 값을 이 범위로 맞춘다. 카페 40–90분 · 식당 60–100분 · 문화시설 50–120분 · 관광지·자연 30–120분 · 레저 60–150분 · 행사 60–150분.

검증 — 평가셋의 checks 와 같은 항목이다. 하나라도 어기면 고친다.

항목 기준
목록 안 장소 모든 ref 가 후보 목록에 있고 중복이 없다
장소 수 시간 범위 1.5시간당 1곳 기준. 최소 2곳, 최대 6곳
영업 도착부터 떠날 때까지 영업 중이다(브레이크 포함). 식당은 마지막 주문 30분 전까지 도착한다
시간 범위 마지막 장소가 끝 시각 + 10분 안에 끝난다
이동 구간마다 위 표의 최대 시간 이하다
반려동물 §3.3 규칙을 지킨다. 미확인 곳에는 경고를 붙인다
실내 비율 비 조건이면 50% 이상이다. 실내 후보가 모자라면 위반으로 고치지 않고 notices 로 알린다
다양성 후보가 허락하면 서로 다른 분류가 2개 이상이다

고치는 순서

  1. 순서 바꾸기: 모든 순서(6곳이면 720가지)를 따져, 위반이 없고 이동이 짧으며 AI 가 정한 순서와 가장 비슷한 것을 고른다. 식사 시간대 규칙을 지킨다.
  2. 머무는 시간 조정: 범위 안에서 늘리거나 줄여 시간 범위에 맞춘다.
  3. 교체·제외: 위반한 장소를 바꾸기 후보로 바꾸고, 안 되면 뺀다(최소 2곳까지).
  4. AI 재호출: 그래도 안 되면 위반 목록을 붙여 AI 를 한 번 더 부른다(제한 시간이 남았을 때만).
  5. 결과: 2곳 미만이면 failed + AI_003 이다. AI 가 남긴 이유는 failure.notices 로 함께 준다("대형견 동반이 확인된 실내 전시가 없어요"). 장소를 줄였으면 notices 에 이유를 남기고 partial 로 낸다.

3.7 결과

GET /api/course-generations/{id} 의 result 다. 저장하기 전의 초안이고 코스(course)가 아니다.

필드 만드는 쪽 · 내용
outcome course · partial
title, summary, notices AI
weather 기상청 — 날짜 · 강수확률 · 기온 · 발표 시각
window, mobility, totals 코드 — 장소 수 · 전체 시간 · 이동 시간 합계 · 반려 동반 확인한 곳 수
stops[] kind(place · event), placeId / eventId, 이름, 분류, 사진(라이선스 포함), arrive · leave · minutes, reason(AI), facts(그날 영업시간과 출처·확인일, 반려 조건과 확인일, 실내외, 행사 기간), warnings(hours_unknown · pet_unknown · event_time_unknown · event_ends_early), legToNext(분 · 이동 수단 · 추정 여부)
alternates[] 바꿀 장소 번호와 stop 과 같은 필드. 같은 시간대로 검증했고, 앞뒤 구간 이동 시간도 다시 계산해 둔다 — 앱이 AI 호출 없이 바꾼다
sources 출처 문구(한국관광공사 TourAPI · 공공누리 유형, 기상청 예보 발표 시각, KOPIS 등)
  • 결과의 사실 값은 생성 시점의 스냅샷이다. 저장한 코스는 그 뒤로 최신 장소 값을 읽는다(코스 설계에서 정한다).
  • 결과 화면 아래 "AI가 공공데이터를 바탕으로 만든 코스예요 …" 문구가 AI 생성 표시다. 저장·공유한 코스에도 'AI가 만든 코스' 표시를 남긴다(코스의 origin = ai, 코스 설계).

4. API

비로그인으로 쓴다. 모든 요청에 X-Device-Id 헤더를 보낸다. 앱이 처음 실행할 때 만든 UUID 이고 flutter_secure_storage 에 둔다(헤더 이름은 상수 DEVICE_ID_HEADER). 로그인이 생기면 Bearer 토큰이 있을 때 회원 횟수를 쓴다(OptionalJwtAuthGuard).

엔드포인트 내용 실패
GET /api/course-generations/quota {limit, used, remaining, resetsAt, tier} — 입력 화면 "오늘 2번 만들 수 있어요" —
POST /api/course-conditions 조건 해석(§2). 차감 없음. AI 를 못 쓰면 기본값만 채워 돌려준다 VAL_001
POST /api/course-generations {request, conditions, previousGenerationId?} → 201 {id, status, remaining} VAL_001, AI_001, AI_002, AI_004
GET /api/course-generations/{id} {status, stage, progress: {found, kept}, result, failure: {code, notices}, remaining}. 만든 기기·사용자만 볼 수 있다(다르면 RES_001) RES_001
POST /api/course-generations/{id}/cancellation 취소. AI 호출을 끊는다. 차감 없음 RES_001
PUT /api/course-generations/{id}/rating {value: like · dislike} — "이 코스, 마음에 드세요?" RES_001, VAL_001
  • status 는 queued · running · succeeded · failed · canceled 다. 생성이 실패해도 조회 자체는 200 이고, 이유는 failure.code 로 준다(문구는 에러 코드 사전).
  • 저장은 코스 API(POST /api/courses, 코스 설계)에 generationId 와 바꾼 후보를 함께 보낸다. 로그인이 필요하다.
  • 요청 제한: 생성은 분당 5회, 조건 해석은 분당 20회다. 진행 조회는 1~2초 간격이라 기본 한도(분당 100회) 대신 분당 300회로 따로 둔다. 통신사 NAT 뒤에서는 여러 사용자가 같은 IP 를 쓰기 때문이다.
  • Swagger 는 기존 래퍼(ApiStandardResponse · ApiErrorResponse)로 문서화한다.

5. 횟수와 비용 보호

장치 규칙
하루 횟수 비로그인 2회(기기 기준), 로그인 5회(사용자 기준)(§13). 한국 시간 자정에 초기화한다
차감 결과가 나온 생성(succeeded)만 차감한다. 실패·취소·후보로 바꾸기·조건 해석은 차감하지 않는다
동시 생성 기기·사용자마다 1건. 진행 중이면 AI_004
시도 상한 실패·취소를 포함한 하루 시도는 횟수의 3배까지다. 취소를 반복해 비용만 쓰는 것을 막는다
하루 비용 상한 그날 쓴 금액(토큰 × 가격 상수의 합. 조건 해석 포함)이 AI_DAILY_BUDGET_USD 를 넘으면 새 생성을 AI_002 로 막고 error 로그를 남긴다
월 한도 Anthropic Console 에 월 사용 한도를 건다. 마지막 안전장치다(사용자 설정)
  • 앱을 지웠다 깔면 기기 ID 가 새로 생겨 비로그인 횟수를 다시 받을 수 있다. MVP 는 이 정도는 감수하고 하루 비용 상한으로 막는다. 앱 무결성 검사(Play Integrity · App Attest)는 2단계다.
  • 기기 ID 는 원문이 아니라 SHA-256 해시로 저장한다.

6. 저장

6.1 ai_course_generation

컬럼 설명
id uuid PK
user_id 로그인 사용자(인증 모듈이 생기면 FK) 또는 null
device_hash 기기 ID 해시
lang ko · en
request_text 사용자가 쓴 문장. 전화번호·이메일 모양은 가린 뒤 저장하고 AI 에 보낸다. 보관 기간이 지나면 지운다(§10)
conditions jsonb
status, stage §3.1
found_count, kept_count 진행 표시 숫자
result jsonb(§3.7). 실패해도 AI 가 남긴 notices 는 여기에 둔다
failure_code AI_002 · AI_003
counted 횟수를 차감했는지
previous_id 다시 만들기라면 이전 생성
model, prompt_version
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, cost_usd 비용 계산 · 하루 상한
latency_ms, llm_calls, repairs 품질 지표(어느 고치기 단계를 거쳤는지 포함)
rating, rated_at like · dislike
created_at, finished_at
  • 인덱스: (device_hash, created_at), (user_id, created_at), (status).
  • 하루 사용량과 비용은 이 테이블에서 센다. 집계 테이블은 따로 두지 않는다.

6.2 지원 지역

  • 동네 3곳(도심 · 수성못 · 앞산)은 앱의 Neighborhood 와 같은 키·좌표를 백엔드 상수로 둔다. 별칭(동성로 · 김광석길 · 대명동 카페거리 …)도 함께 둔다.
  • 대구 영역은 shared/utils/geo.ts 를 그대로 쓴다. 지역을 넓히면 이 상수와 앱 목록을 함께 늘린다.

7. 에러 코드 (신규)

error-codes.ts 의 DOMAIN_ERROR_CODES 에 등록하고 npm run sync:error-codes 를 실행한다.

코드 HTTP 상황 메시지(안)
AI_001 429 하루 횟수를 다 씀 "오늘 만들 수 있는 AI 코스를 모두 만들었어요. 내일 다시 만들 수 있어요."
AI_002 503 AI 를 쓸 수 없음(장애 · 제한 시간 초과 · 하루 비용 상한 · 키 없음) "지금은 AI 코스를 만들 수 없어요. 잠시 후 다시 시도해 주세요."
AI_003 422 조건에 맞는 장소가 부족해 코스를 만들지 못함 "조건에 맞는 장소를 찾지 못했어요. 시간이나 조건을 바꿔 다시 만들어 보세요."
AI_004 409 이미 만드는 중 "이미 코스를 만들고 있어요. 잠시만 기다려 주세요."
  • 앱은 AI_001 을 받으면 비로그인일 때 로그인 안내를 함께 띄운다(코드로 분기하고 문구는 사전에서 가져온다).
  • AI_002 · AI_003 이 생성 작업 안에서 생기면 응답 상태 코드가 아니라 failure.code 로 전달된다. AI_003 에는 이유(failure.notices)가 붙을 수 있다.
  • 앱이 분기하는 코드(AI_001)는 앱 쪽 계약 테스트 목록에 넣는다(체크리스트 3절).

8. 모델과 비용

가격 (2026-11-05 Anthropic 공식 가격표, 1M 토큰당 USD)

모델 입력 캐시 읽기 출력 공식 설명
Claude Haiku 5.5 (claude-haiku-5-5) 0.10 0.01 0.50 가장 빠르다. 분류·추출 같은 대량·저지연 작업용. 입력이 10만 토큰을 넘으면 비싸진다(우리는 해당 없음)
Claude Sonnet 5.5 (claude-sonnet-5-5) 2 0.10 10 빠르다. 속도와 지능의 균형
Claude Opus 5.5 (claude-opus-5-5) 4 0.20 20 보통. 긴 에이전트 작업용

비용 추정

가정: 생성 1회는 입력 7,000 토큰(시스템 2,000 + 후보·조건 5,000)과 출력 2,500 토큰(추론 포함)이다. 조건 해석 1회는 입력 1,500 토큰과 출력 200 토큰이다. 환율은 1달러 = 1,400원으로 잡았다.

Haiku 5.5 Sonnet 5.5
코스 생성 1회 약 $0.002 (3원) 약 $0.04 (50원대)
조건 해석 1회 약 $0.0003 (1원 미만) — (Haiku 고정)
생성 하루 100회 × 30일 약 $6 약 $120
생성 하루 1,000회 × 30일 약 $60 약 $1,200

모델 고르는 방법

  • AI 가 맡는 일은 좁다. 40곳 안에서 고르기만 하고, 순서·시간·사실 검증은 코드가 한다. 그래서 Haiku 5.5 부터 평가하고, 목표(§9)에 못 미치면 Sonnet 5.5 로 올린다. Opus 는 쓰지 않는다(Sonnet 의 2배 비용에 더 느리다).
  • 비교 항목은 자동 채점 통과율, 반려 조건 위반, 지어낸 정보, 이유의 자연스러움(AI 채점), 지연(중앙값 · 95%), 1회 비용이다.
  • 지연 목표는 생성 시작부터 결과까지 95%가 20초 안이다(디자인의 "보통 10~20초 걸려요").
  • 모델 이름은 설정(AI_COURSE_MODEL)으로 바꾼다. 모델을 바꾸면 평가를 다시 돌린다.

9. 평가 (docs/ai-eval/)

  • 평가셋을 대구로 옮겼다(v2 초안, 2026-11-05). v1 62개 중 60개가 서울·부산·제주 등 다른 지역이라 장소 DB(대구)로는 돌릴 수 없었다. v1 은 지역을 넓힐 때 다시 쓴다. v2 는 course-eval-v2.jsonl 62개다.
    • 지역을 대구 동네(도심 · 수성못 · 앞산)와 대표 장소(팔공산 · 동촌유원지 · 이월드 · 서문시장 등)로 바꾸고, 날짜를 2026-11-05 기준으로 다시 잡았다.
    • 사례마다 사용자가 확인한 조건(conditions)과 문장에서 뽑아야 할 항목(inferred)을 적어 조건 해석도 함께 채점한다.
    • expected_behavior 는 흐름에 맞춰 나눈다. ask_clarification · decline_out_of_scope(8개)는 조건 해석 단계의 issues 로, refuse_part · partial_with_notice · no_course 는 생성 단계의 notices 로 시험한다.
    • 만들면서 확인한 데이터 한계(반려 동반 실내 0곳 · 시장은 큐레이션 필요 · 11월 대구 축제 없음)는 평가셋 README 에 적었다.
    • 사용자가 사는 지역이므로 실제로 할 법한 요청인지 사용자 검토를 받는다.
  • 후보 스냅샷을 고정한다. 장소 DB 는 동기화로 계속 바뀐다. 첫 실행 때 사례별 후보 목록을 파일로 저장하고, 이후 모델 비교는 같은 후보로 한다. requires 에 적은 장소가 후보에 없으면(큐레이션 전) 그 사례는 '데이터 준비 안 됨'으로 따로 센다.
  • 채점: 자동 채점(코드. §3.6 검증과 같은 함수) → AI 채점자(이유가 데이터와 맞는지 · 자연스러운지, 1–5점) → 사람 검토(무작위 10개).
  • 목표 (v1 README 그대로): 자동 채점 통과 95% 이상, 반려 조건 위반 0건, 지어낸 정보(영업시간 · 동반 여부 · 가격) 0건.
  • 실행: npm run ai:eval -- --model=<id>(예정). 키가 필요해 사용자가 ! 로 실행한다. 62개 × 두 모델에 채점자까지 수 달러 수준이다.

10. 개인정보·법·안전

항목 정한 것
AI 에 보내는 것 요청 문장(전화번호·이메일 모양은 가림), 조건(날짜·시간·인원·반려동물·이동 수단), 후보 정보(공공데이터 · 우리가 쓴 추천 이유), 날씨
AI 에 보내지 않는 것 좌표(사용자 위치 포함), 기기 ID, 사용자 ID, 카카오 데이터
국외 이전 요청 문장에 개인정보가 섞일 수 있다. 처리방침에 'AI 처리 위탁 · 국외 이전(Anthropic, 미국)'을 적는다. Anthropic API 의 데이터 보존·학습 정책은 처리방침을 쓸 때 확인한다
AI 생성 표시 입력 화면에서 AI 가 코스를 짠다고 알리고, 결과 화면과 저장한 코스에 AI 생성 표시를 한다. 세부 방식(문구·위치)은 출시 전에 AI 기본법 시행령·안내서로 확인한다
보관 request_text 와 result 는 90일 보관한 뒤 문장은 지우고 지표만 남긴다(§13). 매일 정리 작업을 돌린다
로그 생성마다 id · 모델 · 토큰 · 비용 · 지연 · 결과만 남긴다. 요청 문장과 프롬프트 전체는 로그에 남기지 않는다(로깅 규칙)
프롬프트 주입 요청 문장은 '사용자 요청' 칸에만 넣는다. 출력은 스키마로 묶여 있고 장소는 번호로만 고르므로, 문장으로 목록 밖 장소를 넣게 만들 수 없다. 평가셋 X04 로 시험한다
키 ANTHROPIC_API_KEY 는 backend/.env.{env} 에만 둔다. 로그·응답에 남기지 않는다

11. 설정

키 위치 기본값 설명
ANTHROPIC_API_KEY backend/.env.{env} — 운영에서는 필수(없으면 기동 실패). 개발에서 없으면 AI API 가 AI_002 를 준다
AI_COURSE_MODEL 루트 .env → compose 평가 뒤 결정(Haiku 5.5 부터 시험) 코스 생성 모델
AI_CONDITION_MODEL 루트 .env → compose claude-haiku-5-5 조건 해석 모델
AI_DAILY_LIMIT_GUEST · AI_DAILY_LIMIT_MEMBER 루트 .env → compose 2 · 5 §5
AI_DAILY_BUDGET_USD 루트 .env → compose 개발 3 하루 비용 상한(정수 달러)
AI_TIMEOUT_MS 루트 .env → compose 30000 AI 호출 제한 시간
  • 숫자는 parsePositiveInt 로 읽는다(빈 문자열 방어). 새 필수 값은 env.validation.ts 와 테스트에 함께 추가한다.
  • SDK(@anthropic-ai/sdk)는 컨테이너 안에서 설치하고, 잠금 파일을 npm 10 으로 확인한다(nestjs 스킬의 의존성 주의 항목).

12. MVP 에서 하지 않는 것

  • 여러 날 코스, 숙소 추천(숙박은 AI 대상이 아니다 — 장소 DB §2.1)
  • 공연 회차 시각에 맞춘 코스(KOPIS 공연은 후보에서 뺀다)
  • 실제 경로 API(대중교통·자동차 길찾기). 직선거리 추정으로 시작한다
  • 대화로 고치기("카페만 바꿔 줘"). 지금은 후보로 바꾸기 · 조건 수정 · 다시 만들기로 대신한다
  • 사용 기록 기반 개인화, 카카오 검색 장소를 AI 후보로 쓰기(금지)
  • 스트리밍(SSE) 진행 표시, 별도 큐 서버(Redis). 서버를 늘릴 때 다시 본다

13. 정한 것 (2026-11-05)

v0.1 의 추천안을 사용자가 모두 그대로 골랐다.

# 항목 결정 이유
1 하루 횟수 비로그인 2회 · 로그인 5회, 결과가 나온 생성만 차감 기존 합의(비로그인 1~2회, 로그인하면 확대) 안에서 가장 단순하다. Haiku 라면 1회 3원이라 여유가 있다
2 비용 상한 개발은 하루 $3, Console 월 한도는 $50 부터. 운영 값은 출시 전에 정한다 키 유출이나 버그로 비용이 튀는 것을 막는다
3 영업시간 미확인 장소 자연·관광지·행사만 후보로 두고 식당·카페·문화시설은 뺀다 닫힌 식당으로 보내는 것이 가장 큰 실패다. 대구 관광공사 장소의 시간표 해석률은 87%다
4 반려동물 엄격도 실내는 동반이 확인된 곳만, 야외는 미확인도 허용(경고 표시), 불가·크기 불일치는 뺀다 평가셋 pet_policy: strict(확인된 곳만 '동반 가능' 표시)와 맞는다. 대구 반려 데이터가 적어 야외까지 막으면 코스가 거의 나오지 않는다
5 이동 시간 MVP 는 직선거리 추정('약 N분'), 경로 API 는 2단계 키·비용·약관 검토 없이 시작할 수 있다. 걷기는 오차가 작다
6 공연 MVP AI 후보에서 뺀다 회차 시각을 해석하기 전에는 그 시간에 공연한다고 보장할 수 없다
7 요청 문장 보관 90일 품질 검토에 충분하고, 필요 이상 오래 갖고 있지 않는다
8 기본 모델 평가로 정하되 Haiku 5.5 부터 시험 §8

함께 필요한 준비(사용자): Anthropic Console 에서 API 키를 만들고 월 한도를 건 뒤, backend/.env.development 에 ANTHROPIC_API_KEY 를 넣는다.

14. 구현 순서

  1. 평가셋 v2(대구) — 지역·날짜 바꾸기, 사용자 검토 (키 불필요). 2026-11-05 초안을 만들었다
  2. 순수 로직 — 후보 거르기 · 이동 시간 · 시간표 · 검증 · 고치기 + 단위 테스트, 계절별 시간표를 날짜로 고르기(§3.3) (키 불필요)
  3. 날씨(기상청 단기예보 · 격자 변환 · 캐시) + 공휴일 표 (공공데이터 키 — 실제 호출 확인은 사용자가 ! 로)
  4. 테이블 · 에러 코드 · API 골격(가짜 AI 클라이언트) + e2e (키 불필요, CI 에서도 돈다)
  5. Anthropic 연동 — SDK 설치(컨테이너), 프롬프트 v1, 조건 해석
  6. 평가 실행(사용자가 !) → 모델 · effort 결정 → 이 문서 갱신
  7. Flutter 화면 8–10 연결
  8. 출시 준비 — 처리방침(국외 이전) · AI 표시 문구 · Console 한도 · 보관 정리 작업