feat(backend): AI 코스 시간표를 짜고 검증·고치는 순수 로직 추가 #11

Merged
ttipo merged 1 commits from feat/course-planner into main 2026-11-06 12:34:51 +09:00
Owner

무엇을 · 왜

AI 코스 생성에서 코드가 맡는 부분(설계 §3.3–§3.7)을 순수 함수로 만든다. 구현 순서의 2단계(설계 §14)다. 아직 API·앱에는 연결하지 않았고, 연결은 4단계에서 한다.

AI 는 후보 번호로 장소와 머무는 시간만 고른다. 영업시간·이동·반려동물 조건 검증과 순서 고치기는 코드가 맡는다. 그래야 닫힌 식당으로 보내거나 지어낸 사실이 화면에 나가지 않는다. 키 없이 시험할 수 있게 DB·AI 와 떨어진 순수 함수로 먼저 만든다.

흐름 — backend/src/modules/course-generation/planner/

단계 파일 하는 일
거르기 filter.ts 반경, 그날 진행하는 행사, 공휴일 휴무, 반려동물 조건, 그날 영업을 본다(아래 목록). 뺀 이유별 개수도 돌려준다
줄이기 shortlist.ts 장소와 행사를 합쳐 40곳 이하로 줄이고 P01·E01 번호를 붙인다(아래 목록)
시간표·검증 schedule.ts · travel.ts · availability.ts 도착·떠나는 시각을 계산하고 영업·이동·끝 시각·반려동물을 검증한다(아래 목록)
고치기 plan-course.ts 순서와 머무는 시간을 다시 찾고, 바꾸기 후보로 교체하거나 장소를 뺀다(아래 목록)

거르기

  • 반경: 걸어서 1.5km · 대중교통 6km · 자동차 20km
  • 공휴일 여부는 3단계 공휴일 표에서 받는다
  • 반려동물: 실내는 동반이 확인된 곳만 남긴다
  • 영업: 그날 최소 머무는 시간만큼 여는 곳만 남긴다. 영업시간 미확인은 자연·관광지·행사만 남긴다

줄이기

  • 행사는 가까운 8개까지, 장소는 분류마다 10곳까지 넣고 1순위(확인한 곳)를 먼저 둔다
  • 다시 만들기: 이전 코스의 장소를 빼도 15곳 이상 남으면 빼고, 아니면 목록 뒤로 보낸다

시간표·검증

  • 이동 시간은 직선거리로 추정해 5분 단위로 올린다
  • 도착부터 떠날 때까지 영업 중이어야 한다(브레이크·계절별 시간표 포함)
  • 식당은 마지막 주문 30분 전까지 도착해야 한다
  • 끝 시각 + 10분 안에 끝나야 하고, 구간마다 최대 이동 시간을 넘지 않아야 한다
  • 반려동물 조건을 다시 본다. 식사 시간대는 약한 위반이다

고치기

  • 순서와 머무는 시간을 함께 찾는다. AI 순서에 가까운 것, 이동이 짧은 것, 머무는 시간이 덜 바뀐 것 순으로 고른다
  • 그래도 안 되면 바꾸기 후보로 바꾸거나 장소를 뺀다(최소 2곳까지)
  • 비 조건이면 실내 비율 50% 를 맞춰 보고, 못 맞추면 알린다
  • 바꾸기 후보는 같은 시간대에 넣어 검증해 둔다

결과는 세 가지다.

  • course
  • partial: 장소를 뺐거나 AI 가 알림을 남겼다
  • failed: 2곳 미만이다. 위반 목록을 함께 주므로 생성 작업이 AI 를 다시 부르거나 AI_003 을 낸다

장소마다 경고(hours_unknown · event_time_unknown · pet_unknown · event_last_day)를 붙인다. 코스 합계(장소 수 · 전체 시간 · 이동 시간 · 반려동물 동반이 확인된 장소 수)도 붙인다.

구현하며 정한 것 (설계 §3.4·§3.6·§3.7 에 적었다)

무엇 정한 것 이유
순서를 고칠 때 AI 순서에 가까운 쪽을 이동이 짧은 쪽보다 먼저 고른다 "레스토랑으로 마무리"처럼 요청에 담긴 순서를 지킨다
첫 장소가 늦게 열면 첫 도착을 1시간까지 늦춘다 늦추지 않으면 시작 시각에 이미 문을 연 곳만 첫 장소가 될 수 있다
쇼핑·술집·기타 머무는 시간 30–120 · 60–120 · 30–120분 설계 표에 없었다. 이 분류는 1순위(큐레이션) 장소만 후보가 된다
행사 경고 event_ends_early 대신 event_last_day(그날이 마지막 날) 평가셋 F07 이 "마지막 날임을 알리는가"를 본다
대중교통·자동차의 짧은 구간 걷는 편이 빠르면 걷기로 계산하고, 구간에 실제 수단을 적는다 대중교통은 고정 10분 때문에 300m 도 11분이 넘게 나왔다(걸으면 6분)
AI 가 알림을 남기면 partial 로 낸다 평가셋 partial_with_notice 와 맞춘다
식사 시간대 · 다양성 약한 위반으로 둔다. 이것만으로는 고치지 않는다 11시 점심 같은 AI 의 선택을 바꾸지 않는다. '카페 투어'처럼 한 분류만 고를 수도 있다
계산 상한 한 코스에서 순서 5,000개까지만 따진다 서버가 한 대라 계산이 길면 다른 요청이 기다린다

함께 바꾼 것

  • shared/utils/geo.ts 에 distanceMeters 를 더했다. 장소 반경 검색 SQL 과 같은 하버사인 식이다
  • tsconfig.build.json 에서 테스트용 *.fixtures.ts 를 운영 빌드에서 뺐다

확인 방법

cd backend && npx jest src/modules/course-generation src/shared/utils/geo.spec.ts
  • 테스트를 71개 새로 넣었다. 관광공사 원문 형식의 영업시간 문장(브레이크·계절별·마지막 주문)으로 거르기·시간표·고치기 사례를 확인한다.
  • 계산 시간을 쟀다(2026-11-06, 개발 PC).
    • 6곳 코스 고치기는 대부분 20ms 안이다
    • 가장 나쁜 경우는 약 50ms 다. AI 순서가 거꾸로이면서 시간이 빠듯할 때다
    • 처음 짠 방식은 0.5~0.9초가 걸렸다. 그래서 그날 머물 수 없는 곳을 먼저 빼고, AI 순서에 가까운 순서부터 따지게 바꿨다

영향 범위

  • backend
  • frontend
  • flutter
  • docker / 인프라
  • 규칙 문서(.claude/)

공유 계약은 바뀌지 않는다. 새 코드는 아직 어느 모듈·API 에도 연결되지 않았다. 에러 코드(AI_001~AI_004)는 4단계에서 레지스트리에 넣는다.

체크리스트

  • 린트/분석 통과 (Warning 0) · 변경 영역 테스트 통과: lint · tsc · 백엔드 단위 테스트 333개
  • 주석 한국어, 매직 넘버·하드코딩 문자열 없음: 기준값은 planner.constants.ts 에 모았다
  • 사용자 노출 실패는 모두 에러 코드와 함께 처리: 해당 없음(API 연결 전)
  • 에러 코드를 바꿨다면 npm run sync:error-codes 실행 후 생성 파일까지 커밋: 해당 없음
  • 민감정보 미커밋
  • 로그에 비밀번호·토큰·요청 본문 전체가 남지 않음: 로그를 남기지 않는다

🤖 Generated with Claude Code

## 무엇을 · 왜 AI 코스 생성에서 코드가 맡는 부분(설계 §3.3–§3.7)을 순수 함수로 만든다. 구현 순서의 2단계(설계 §14)다. 아직 API·앱에는 연결하지 않았고, 연결은 4단계에서 한다. AI 는 후보 번호로 장소와 머무는 시간만 고른다. 영업시간·이동·반려동물 조건 검증과 순서 고치기는 코드가 맡는다. 그래야 닫힌 식당으로 보내거나 지어낸 사실이 화면에 나가지 않는다. 키 없이 시험할 수 있게 DB·AI 와 떨어진 순수 함수로 먼저 만든다. ### 흐름 — `backend/src/modules/course-generation/planner/` | 단계 | 파일 | 하는 일 | | --- | --- | --- | | 거르기 | `filter.ts` | 반경, 그날 진행하는 행사, 공휴일 휴무, 반려동물 조건, 그날 영업을 본다(아래 목록). 뺀 이유별 개수도 돌려준다 | | 줄이기 | `shortlist.ts` | 장소와 행사를 합쳐 40곳 이하로 줄이고 P01·E01 번호를 붙인다(아래 목록) | | 시간표·검증 | `schedule.ts` · `travel.ts` · `availability.ts` | 도착·떠나는 시각을 계산하고 영업·이동·끝 시각·반려동물을 검증한다(아래 목록) | | 고치기 | `plan-course.ts` | 순서와 머무는 시간을 다시 찾고, 바꾸기 후보로 교체하거나 장소를 뺀다(아래 목록) | **거르기** - 반경: 걸어서 1.5km · 대중교통 6km · 자동차 20km - 공휴일 여부는 3단계 공휴일 표에서 받는다 - 반려동물: 실내는 동반이 확인된 곳만 남긴다 - 영업: 그날 최소 머무는 시간만큼 여는 곳만 남긴다. 영업시간 미확인은 자연·관광지·행사만 남긴다 **줄이기** - 행사는 가까운 8개까지, 장소는 분류마다 10곳까지 넣고 1순위(확인한 곳)를 먼저 둔다 - 다시 만들기: 이전 코스의 장소를 빼도 15곳 이상 남으면 빼고, 아니면 목록 뒤로 보낸다 **시간표·검증** - 이동 시간은 직선거리로 추정해 5분 단위로 올린다 - 도착부터 떠날 때까지 영업 중이어야 한다(브레이크·계절별 시간표 포함) - 식당은 마지막 주문 30분 전까지 도착해야 한다 - 끝 시각 + 10분 안에 끝나야 하고, 구간마다 최대 이동 시간을 넘지 않아야 한다 - 반려동물 조건을 다시 본다. 식사 시간대는 약한 위반이다 **고치기** - 순서와 머무는 시간을 함께 찾는다. AI 순서에 가까운 것, 이동이 짧은 것, 머무는 시간이 덜 바뀐 것 순으로 고른다 - 그래도 안 되면 바꾸기 후보로 바꾸거나 장소를 뺀다(최소 2곳까지) - 비 조건이면 실내 비율 50% 를 맞춰 보고, 못 맞추면 알린다 - 바꾸기 후보는 같은 시간대에 넣어 검증해 둔다 결과는 세 가지다. - `course` - `partial`: 장소를 뺐거나 AI 가 알림을 남겼다 - `failed`: 2곳 미만이다. 위반 목록을 함께 주므로 생성 작업이 AI 를 다시 부르거나 `AI_003` 을 낸다 장소마다 경고(`hours_unknown` · `event_time_unknown` · `pet_unknown` · `event_last_day`)를 붙인다. 코스 합계(장소 수 · 전체 시간 · 이동 시간 · 반려동물 동반이 확인된 장소 수)도 붙인다. ### 구현하며 정한 것 (설계 §3.4·§3.6·§3.7 에 적었다) | 무엇 | 정한 것 | 이유 | | --- | --- | --- | | 순서를 고칠 때 | AI 순서에 가까운 쪽을 이동이 짧은 쪽보다 먼저 고른다 | "레스토랑으로 마무리"처럼 요청에 담긴 순서를 지킨다 | | 첫 장소가 늦게 열면 | 첫 도착을 1시간까지 늦춘다 | 늦추지 않으면 시작 시각에 이미 문을 연 곳만 첫 장소가 될 수 있다 | | 쇼핑·술집·기타 머무는 시간 | 30–120 · 60–120 · 30–120분 | 설계 표에 없었다. 이 분류는 1순위(큐레이션) 장소만 후보가 된다 | | 행사 경고 | `event_ends_early` 대신 `event_last_day`(그날이 마지막 날) | 평가셋 F07 이 "마지막 날임을 알리는가"를 본다 | | 대중교통·자동차의 짧은 구간 | 걷는 편이 빠르면 걷기로 계산하고, 구간에 실제 수단을 적는다 | 대중교통은 고정 10분 때문에 300m 도 11분이 넘게 나왔다(걸으면 6분) | | AI 가 알림을 남기면 | `partial` 로 낸다 | 평가셋 `partial_with_notice` 와 맞춘다 | | 식사 시간대 · 다양성 | 약한 위반으로 둔다. 이것만으로는 고치지 않는다 | 11시 점심 같은 AI 의 선택을 바꾸지 않는다. '카페 투어'처럼 한 분류만 고를 수도 있다 | | 계산 상한 | 한 코스에서 순서 5,000개까지만 따진다 | 서버가 한 대라 계산이 길면 다른 요청이 기다린다 | ### 함께 바꾼 것 - `shared/utils/geo.ts` 에 `distanceMeters` 를 더했다. 장소 반경 검색 SQL 과 같은 하버사인 식이다 - `tsconfig.build.json` 에서 테스트용 `*.fixtures.ts` 를 운영 빌드에서 뺐다 ## 확인 방법 ```bash cd backend && npx jest src/modules/course-generation src/shared/utils/geo.spec.ts ``` - 테스트를 71개 새로 넣었다. 관광공사 원문 형식의 영업시간 문장(브레이크·계절별·마지막 주문)으로 거르기·시간표·고치기 사례를 확인한다. - 계산 시간을 쟀다(2026-11-06, 개발 PC). - 6곳 코스 고치기는 대부분 20ms 안이다 - 가장 나쁜 경우는 약 50ms 다. AI 순서가 거꾸로이면서 시간이 빠듯할 때다 - 처음 짠 방식은 0.5~0.9초가 걸렸다. 그래서 그날 머물 수 없는 곳을 먼저 빼고, AI 순서에 가까운 순서부터 따지게 바꿨다 ## 영향 범위 - [x] backend - [ ] frontend - [ ] flutter - [ ] docker / 인프라 - [ ] 규칙 문서(.claude/) > 공유 계약은 바뀌지 않는다. 새 코드는 아직 어느 모듈·API 에도 연결되지 않았다. 에러 코드(`AI_001`~`AI_004`)는 4단계에서 레지스트리에 넣는다. ## 체크리스트 - [x] 린트/분석 통과 (Warning 0) · 변경 영역 테스트 통과: lint · tsc · 백엔드 단위 테스트 333개 - [x] 주석 한국어, 매직 넘버·하드코딩 문자열 없음: 기준값은 `planner.constants.ts` 에 모았다 - [x] 사용자 노출 실패는 모두 에러 코드와 함께 처리: 해당 없음(API 연결 전) - [x] 에러 코드를 바꿨다면 `npm run sync:error-codes` 실행 후 생성 파일까지 커밋: 해당 없음 - [x] 민감정보 미커밋 - [x] 로그에 비밀번호·토큰·요청 본문 전체가 남지 않음: 로그를 남기지 않는다 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ttipo added 1 commit 2026-11-06 11:49:57 +09:00
feat(backend): AI 코스 시간표를 짜고 검증·고치는 순수 로직 추가
CI / Backend (lint · test · e2e · build) (pull_request) Successful in 13m30s
CI / Frontend (lint · type-check · test) (pull_request) Successful in 11m28s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m56s
CI / Backend (lint · test · e2e · build) (push) Successful in 14m17s
CI / Frontend (lint · type-check · test) (push) Successful in 12m17s
CI / 에러 코드 SSOT 동기화 검사 (push) Successful in 12m55s
d2cd5cd96e
AI 코스 생성(설계 §3.3–§3.7)에서 코드가 맡는 부분이다. AI 는 후보 번호로 장소와 머무는 시간만 고르고,
영업·이동·반려동물 조건 검증과 순서 고치기는 코드가 해야 닫힌 곳으로 보내거나 지어낸 사실이 화면에 나가지 않는다.
API·AI 연동(4·5단계) 전에 키 없이 시험할 수 있게 DB·AI 와 떨어진 순수 함수로 먼저 만든다.

- 거르기: 반경·행사 기간·공휴일 휴무·반려동물·그날 영업(최소 머무는 시간만큼)
- 줄이기: 장소와 행사를 합쳐 40곳(행사 8개까지, 분류마다 10곳), 다시 만들기면 이전 장소를 빼거나 뒤로
- 시간표·검증: 직선거리 이동 추정, 영업·마지막 주문·끝 시각·구간 이동·반려동물
- 고치기: 순서와 머무는 시간을 함께 찾고(AI 순서와 가까운 것 우선), 안 되면 바꾸기 후보로 바꾸거나 뺀다
- 비 조건 실내 비율, 바꾸기 후보를 같은 시간대로 검증, 장소별 경고

6곳 코스 고치기는 대부분 20ms 안이고 가장 나쁜 경우 약 50ms 였다(서버가 한 대라 따지는 순서를 5,000개로 제한).
구현하며 정한 기준은 설계 §3.4·§3.6·§3.7 에 적었다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ttipo merged commit d2cd5cd96e into main 2026-11-06 12:34:51 +09:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: ttipo/wandergo#11