1414
1515import type { Course , CourseItem , Eat , Spot , Stay , Theme } from "./types" ;
1616import { L , localized } from "@/lib/i18n" ;
17- import { estimateDrive , routePenaltyPoints , type RouteContext , type TravelMode } from "./travel-time" ;
17+ import {
18+ approxTravelTimeLookup ,
19+ estimateDrive ,
20+ routePenaltyPoints ,
21+ type RouteContext ,
22+ type TravelMode ,
23+ type TravelTimeLookup ,
24+ } from "./travel-time" ;
1825
1926export interface ComposeCourseInput {
2027 theme : Theme ;
@@ -41,6 +48,8 @@ export interface ComposeCourseOptions {
4148 startRegion ?: string ;
4249 /** 이동수단. car=자가용/렌터카, transit=대중교통, walk=도보 중심. */
4350 transport ?: TravelMode ;
51+ /** 서버 길찾기 API 또는 근사 계산으로 구성한 이동시간 조회 포트. */
52+ travelTimeLookup ?: TravelTimeLookup ;
4453 /** 하루에 배치할 최대 스팟 수. 기본 3. */
4554 maxSpotsPerDay ?: number ;
4655}
@@ -52,6 +61,7 @@ interface NormalizedComposeOptions {
5261 companion ?: string ;
5362 startRegion ?: string ;
5463 transport : TravelMode ;
64+ travelTimeLookup : TravelTimeLookup ;
5565 maxSpotsPerDay : number ;
5666}
5767
@@ -163,11 +173,16 @@ function routePenalty(original: Spot, candidate: Spot, context: RouteContext): n
163173 return routePenaltyPoints ( originalCoord , candidateCoord , context ) ;
164174}
165175
166- function routeLegPenalty ( prev : Spot | undefined , candidate : Spot , mode : TravelMode ) : number {
176+ function routeLegPenalty (
177+ prev : Spot | undefined ,
178+ candidate : Spot ,
179+ mode : TravelMode ,
180+ lookup : TravelTimeLookup ,
181+ ) : number {
167182 const prevCoord = toRouteCoord ( prev ) ;
168183 const candidateCoord = toRouteCoord ( candidate ) ;
169184 if ( ! prevCoord || ! candidateCoord ) return 0 ;
170- const drive = estimateDrive ( prevCoord , candidateCoord , mode ) ;
185+ const drive = estimateDrive ( prevCoord , candidateCoord , mode , lookup ) ;
171186 if ( ! drive || drive . driveMinutes <= 45 ) return 0 ;
172187 return Math . min ( 30 , ( drive . driveMinutes - 45 ) * 0.35 ) ;
173188}
@@ -265,7 +280,9 @@ function pickScratchSpots(input: ComposeCourseInput, dayCount: number, options:
265280 const bestIndex = remaining
266281 . map ( ( spot , index ) => ( {
267282 index,
268- score : scoreSpot ( input . theme , spot , undefined , options ) - routeLegPenalty ( prev , spot , options . transport ) ,
283+ score :
284+ scoreSpot ( input . theme , spot , undefined , options ) -
285+ routeLegPenalty ( prev , spot , options . transport , options . travelTimeLookup ) ,
269286 } ) )
270287 . sort ( ( a , b ) => b . score - a . score ) [ 0 ] ?. index ;
271288 if ( bestIndex === undefined ) break ;
@@ -293,9 +310,11 @@ function note(changedSpotIds: string[], changedCommerce: boolean): Course["altNo
293310
294311function composeFromTemplate ( input : ComposeCourseInput , options : NormalizedComposeOptions ) : Course {
295312 const base = input . baseCourse as Course ;
313+ const targetDayCount = options . days ?? base . dayCount ;
314+ const templateItems = base . items . filter ( ( it ) => it . day <= targetDayCount ) ;
296315 const spotsById = new Map ( input . spots . map ( ( s ) => [ s . id , s ] ) ) ;
297316 const baseSpotItemsByDay = new Map < number , CourseItem [ ] > ( ) ;
298- for ( const it of base . items ) {
317+ for ( const it of templateItems ) {
299318 if ( it . kind !== "spot" ) continue ;
300319 baseSpotItemsByDay . set ( it . day , [ ...( baseSpotItemsByDay . get ( it . day ) ?? [ ] ) , it ] ) ;
301320 }
@@ -306,7 +325,7 @@ function composeFromTemplate(input: ComposeCourseInput, options: NormalizedCompo
306325 let changedCommerce = false ;
307326
308327 const spotRegionByDay = new Map < number , string > ( ) ;
309- const firstPass : CourseItem [ ] = base . items . map ( ( it ) => {
328+ const firstPass : CourseItem [ ] = templateItems . map ( ( it ) => {
310329 if ( it . kind !== "spot" ) return { ...it } ;
311330 const original = spotsById . get ( it . refId ) ;
312331 const daySpotItems = baseSpotItemsByDay . get ( it . day ) ?? [ ] ;
@@ -319,7 +338,7 @@ function composeFromTemplate(input: ComposeCourseInput, options: NormalizedCompo
319338 input . alternativesBySpot ?. [ it . refId ] ?? [ ] ,
320339 usedSpots ,
321340 options ,
322- { prev, next, mode : options . transport } ,
341+ { prev, next, mode : options . transport , lookup : options . travelTimeLookup } ,
323342 ) ;
324343 if ( ! picked ) return { ...it } ;
325344 usedSpots . add ( picked . id ) ;
@@ -350,6 +369,7 @@ function composeFromTemplate(input: ComposeCourseInput, options: NormalizedCompo
350369
351370 return {
352371 ...base ,
372+ dayCount : targetDayCount ,
353373 altNote : note ( changedSpotIds , changedCommerce ) ?? base . altNote ,
354374 items : sortItems ( items ) ,
355375 } ;
@@ -401,6 +421,7 @@ export function composeCourse(input: ComposeCourseInput, options: ComposeCourseO
401421 companion : options . companion ,
402422 startRegion : options . startRegion ,
403423 transport : options . transport ?? "car" ,
424+ travelTimeLookup : options . travelTimeLookup ?? approxTravelTimeLookup ,
404425 maxSpotsPerDay : options . maxSpotsPerDay ?? spotsPerDayForPace ( options . pace ) ,
405426 } ;
406427
0 commit comments