Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,64 @@ import io.github.hangillab.hangil.feature.home.R as HomeR
class FinishSourceReviewRoomFlowTest {
@get:Rule val compose = createAndroidComposeRule<ComponentCatalogActivity>()

@Test fun nativeNavigationCourseKeepsTheSelectedPublicRoadAfterRoomReopen() {
assumeManual()
fixture(courseId = "pleos-pangyo-yangjae") { f ->
val ended =
runBlocking {
val result = f.store.sessions.finish(f.active, TIME + 90_000) { true }
check(result is SessionFinishResult.Finished)
result.session
}
val course = checkNotNull(NextPracticeSeed.resolve(f.store.catalog, ended)).practice.course
check(course.navigationRoute != null)
val road = course.roadSnapshots.single()
check(road.id == "osm-way-619101472-v5")
val before = f.snapshot()
var ui = f.mount(f.store.sessions, f.store.reviewRoads)
ui.openExact(ended)
ui.ready(ended)
ui.touch("history_review_road")
ui.waitTag("review_road_empty")
ui.touch("review_road_candidate_0")
check(
runBlocking {
f.store.reviewRoads
.observe(ended.sessionId)
.first()
} == null,
)
ui.touch("review_road_save")
ui.waitTag("review_road_stored_title")
check(ui.text("review_road_stored_title") == road.title)
val saved =
checkNotNull(
runBlocking {
f.store.reviewRoads
.observe(ended.sessionId)
.first()
},
)
check(saved.source == ended && saved.road == road && saved.origin == "USER_SELECTION" && saved.sampleAnchor == null)
ui.dispose()
runBlocking { f.store.reopen() }
ui = f.mount(f.store.sessions, f.store.reviewRoads)
ui.openExact(ended)
ui.ready(ended)
ui.touch("history_review_road")
ui.waitTag("review_road_stored_title")
check(ui.text("review_road_stored_title") == road.title)
check(
runBlocking {
f.store.reviewRoads
.observe(ended.sessionId)
.first()
} == saved,
)
check(f.snapshot() == before)
}
}

@Test fun committedFlowBeforeAcknowledgementReviewsTheFinishedSourceEvenAfterANewerRecordExists() {
assumeManual()
fixture { f ->
Expand Down Expand Up @@ -846,11 +904,14 @@ class FinishSourceReviewRoomFlowTest {
}
}

private fun fixture(body: (Fixture) -> Unit) {
private fun fixture(
courseId: String? = null,
body: (Fixture) -> Unit,
) {
val f = Fixture(OwnedPracticeTestStore.create(InstrumentationRegistry.getInstrumentation().targetContext))
var primary: Throwable? = null
try {
f.seed()
f.seed(courseId)
body(f)
} catch (failure: Throwable) {
primary = failure
Expand Down Expand Up @@ -894,7 +955,7 @@ class FinishSourceReviewRoomFlowTest {
var ui: FinishSourceReviewBoundaryTest.Driver? = null
val memoQueries = ConcurrentLinkedQueue<String>()

fun seed() =
fun seed(courseId: String?) =
runBlocking {
check(
store.schemaVersion() == SCHEMA_VERSION &&
Expand All @@ -904,7 +965,7 @@ class FinishSourceReviewRoomFlowTest {
.isEmpty(),
)
check(store.preparation.observeSelection().first() == null)
val practice = prepared(0)
val practice = prepared(0, courseId)
val start = store.sessions.start(practice, TIME + 1_000) { true }
check(start is SessionStartResult.Started)
active = start.session
Expand All @@ -917,8 +978,11 @@ class FinishSourceReviewRoomFlowTest {
check(store.courseCompletions.observe(active.sessionId).first() == null)
}

private suspend fun prepared(taskIndex: Int): io.github.hangillab.hangil.core.domain.PreparedPractice {
val course = store.catalog.courses.first()
private suspend fun prepared(
taskIndex: Int,
courseId: String? = null,
): io.github.hangillab.hangil.core.domain.PreparedPractice {
val course = if (courseId == null) store.catalog.courses.first() else store.catalog.courses.single { it.id == courseId }
val goal = store.catalog.goals.first { it.id in course.goalIds }
check(course.tasks.size > taskIndex)
val selection = PracticeSelection(goal.id, course.id, course.version, course.briefingVersion, course.tasks[taskIndex].id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ class MapInputLifecycleTest {
val catalog = LocalPracticeCatalog()
val course = catalog.courses.single { it.id == "pleos-pangyo-yangjae" }
val route = checkNotNull(course.navigationRoute)
assertTrue("시연 preview로 우회하지 않는 현재 nav-only 원본이어야 합니다", course.mapPreview == null)
assertTrue("시연 지도 대신 내비 요청 장소 원본을 사용해야 합니다", course.mapPreview == null)
val source = fixture.preview.source as MapPreview.Source.NavigationRequest
assertTrue(
"전체 요청 원본과 코스 버전을 보존해야 합니다",
Expand Down Expand Up @@ -172,6 +172,11 @@ class MapInputLifecycleTest {
assertTrue("추가 시연 지점을 만들면 안 됩니다", nodes("map_point_2").isEmpty() && nodes("map_demo_badge").isEmpty())
assertPoiText("map_navigation_request_badge", "내비 요청 장소")
assertPoiText("map_navigation_request_scope", "내비에 요청할 장소이며 실제 안내 경로와 현재 위치는 표시하지 않아요.")
assertEquals(course.roadSnapshots, fixture.preview.roads)
assertPoiText("map_road_badge", "공개 OSM 도로 구간")
assertPoiText("map_road_0", "1. 창업로40번길")
assertPoiText("map_road_attribution", "도로 자료: © OpenStreetMap 기여자, ODbL 1.0")
awaitRoadLines(fixture.preview, initial.view, null)
val directory = File(instrumentation.targetContext.cacheDir, "screenshots/navigation-request-pois-${UUID.randomUUID()}")
check(directory.mkdirs()) { "POI_MAP_PRIVATE_CACHE" }
capturePoiRegion(directory, "pois-map.png", "map_view")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class LocalPracticeCatalog
PracticeTask("stops", "경유지 순서 확인하기", "출발 전에 경유지와 최종 목적지의 순서를 확인해 보세요."),
),
goalIds = setOf("commute", "shopping"),
roadSnapshots = listOf(LocalRoadSnapshots.pangyoWaypoint),
// POI identifiers and routing coordinates from the official NaviHelper waypoint example.
// Pleos resolves the waypoint ID to the Software Dream Center in active guidance.
// https://document.pleos.ai/docs/pleos-only/connect-sdk-pleos/NaviHelper/route-with-waypoints
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,96 @@ internal object LocalRoadSnapshots {
CourseRoadSnapshot.Node(436850150L, 6, 37.5756733, 126.9742918),
),
)

// Copyright OpenStreetMap contributors, ODbL 1.0.
// https://api.openstreetmap.org/api/0.6/way/619101472/5.json
// Independent public road snapshot. The current access and Pleos guidance path are unverified.
val pangyoWaypoint =
CourseRoadSnapshot(
id = "osm-way-619101472-v5",
title = "창업로40번길",
highway = "residential",
osmWayId = 619101472L,
osmWayVersion = 5,
capturedAtEpochMillis = 1794440075031L,
nodes =
listOf(
CourseRoadSnapshot.Node(5785292092L, 3, 37.4107925, 127.0948633),
CourseRoadSnapshot.Node(14081613437L, 2, 37.4108116, 127.0946083),
CourseRoadSnapshot.Node(14084804748L, 1, 37.4108137, 127.0945693),
CourseRoadSnapshot.Node(14081613445L, 2, 37.4108163, 127.0945144),
CourseRoadSnapshot.Node(14081613438L, 2, 37.4108204, 127.0944642),
CourseRoadSnapshot.Node(14081613444L, 1, 37.4108273, 127.0944065),
CourseRoadSnapshot.Node(14081613436L, 1, 37.4108413, 127.0943284),
CourseRoadSnapshot.Node(5850817444L, 2, 37.4108636, 127.0942273),
CourseRoadSnapshot.Node(14081613443L, 1, 37.4108929, 127.0941207),
CourseRoadSnapshot.Node(5850817406L, 2, 37.4109122, 127.0940621),
CourseRoadSnapshot.Node(14081613442L, 1, 37.4109331, 127.0940148),
CourseRoadSnapshot.Node(14081613441L, 1, 37.4109541, 127.0939743),
CourseRoadSnapshot.Node(14081613440L, 1, 37.4109864, 127.0939267),
CourseRoadSnapshot.Node(14081613439L, 1, 37.4112145, 127.0936002),
CourseRoadSnapshot.Node(14081613446L, 1, 37.4112843, 127.0934963),
CourseRoadSnapshot.Node(14081613452L, 1, 37.4114015, 127.0933045),
CourseRoadSnapshot.Node(14081613451L, 1, 37.4114194, 127.0932782),
CourseRoadSnapshot.Node(14081613450L, 1, 37.4114463, 127.0932455),
CourseRoadSnapshot.Node(14081613449L, 1, 37.4114745, 127.0932153),
CourseRoadSnapshot.Node(14081613453L, 1, 37.4114906, 127.0932006),
CourseRoadSnapshot.Node(14081613448L, 1, 37.4115056, 127.0931871),
CourseRoadSnapshot.Node(14081613447L, 1, 37.4115421, 127.0931627),
CourseRoadSnapshot.Node(14081613457L, 1, 37.4116208, 127.0931224),
CourseRoadSnapshot.Node(14081613456L, 1, 37.4116475, 127.0931104),
CourseRoadSnapshot.Node(14081613455L, 1, 37.4116789, 127.0931008),
CourseRoadSnapshot.Node(14081613454L, 1, 37.4117096, 127.0930944),
CourseRoadSnapshot.Node(14081613458L, 1, 37.4117465, 127.0930891),
CourseRoadSnapshot.Node(14081613459L, 1, 37.4117700, 127.0930881),
CourseRoadSnapshot.Node(14081613460L, 1, 37.4117931, 127.0930892),
CourseRoadSnapshot.Node(14081613461L, 1, 37.4118213, 127.0930916),
CourseRoadSnapshot.Node(5850817408L, 2, 37.4118498, 127.0930967),
CourseRoadSnapshot.Node(14081613462L, 1, 37.4118737, 127.0931027),
CourseRoadSnapshot.Node(14081613463L, 1, 37.4119014, 127.0931110),
CourseRoadSnapshot.Node(14081613465L, 1, 37.4119312, 127.0931224),
CourseRoadSnapshot.Node(14081613466L, 1, 37.4119675, 127.0931373),
CourseRoadSnapshot.Node(14081613467L, 1, 37.4119980, 127.0931515),
CourseRoadSnapshot.Node(14081613469L, 1, 37.4120159, 127.0931603),
CourseRoadSnapshot.Node(14081613468L, 1, 37.4120362, 127.0931719),
CourseRoadSnapshot.Node(5850817409L, 2, 37.4120621, 127.0931903),
CourseRoadSnapshot.Node(14081613470L, 1, 37.4120882, 127.0932112),
CourseRoadSnapshot.Node(14081613471L, 1, 37.4121197, 127.0932390),
CourseRoadSnapshot.Node(14081613472L, 1, 37.4121524, 127.0932713),
CourseRoadSnapshot.Node(14081613473L, 1, 37.4121787, 127.0932981),
CourseRoadSnapshot.Node(5850817423L, 2, 37.4122059, 127.0933281),
CourseRoadSnapshot.Node(14081613474L, 1, 37.4122283, 127.0933563),
CourseRoadSnapshot.Node(14081613475L, 1, 37.4122497, 127.0933852),
CourseRoadSnapshot.Node(14081613476L, 1, 37.4122696, 127.0934135),
CourseRoadSnapshot.Node(14081613477L, 1, 37.4122863, 127.0934406),
CourseRoadSnapshot.Node(14081613478L, 1, 37.4123029, 127.0934713),
CourseRoadSnapshot.Node(14081613479L, 1, 37.4123179, 127.0935012),
CourseRoadSnapshot.Node(14081613480L, 1, 37.4123334, 127.0935326),
CourseRoadSnapshot.Node(14081613481L, 1, 37.4123500, 127.0935689),
CourseRoadSnapshot.Node(5850817410L, 2, 37.4123665, 127.0936044),
CourseRoadSnapshot.Node(5850817424L, 2, 37.4124143, 127.0937125),
CourseRoadSnapshot.Node(5850817425L, 2, 37.4124909, 127.0938837),
CourseRoadSnapshot.Node(5850817419L, 2, 37.4126072, 127.0941395),
CourseRoadSnapshot.Node(5850817422L, 2, 37.4126413, 127.0942155),
CourseRoadSnapshot.Node(5850817427L, 2, 37.4126690, 127.0942869),
CourseRoadSnapshot.Node(5850817411L, 2, 37.4126962, 127.0943601),
CourseRoadSnapshot.Node(14081613500L, 1, 37.4127069, 127.0944013),
CourseRoadSnapshot.Node(5850817418L, 2, 37.4127145, 127.0944425),
CourseRoadSnapshot.Node(14081613501L, 1, 37.4127188, 127.0944854),
CourseRoadSnapshot.Node(5850817428L, 2, 37.4127231, 127.0945359),
CourseRoadSnapshot.Node(5850817414L, 2, 37.4127238, 127.0945930),
CourseRoadSnapshot.Node(14081613502L, 1, 37.4127233, 127.0946550),
CourseRoadSnapshot.Node(5850817420L, 2, 37.4127173, 127.0947090),
CourseRoadSnapshot.Node(14081613503L, 1, 37.4127111, 127.0947516),
CourseRoadSnapshot.Node(5850817415L, 2, 37.4126995, 127.0947994),
CourseRoadSnapshot.Node(5850817421L, 2, 37.4126786, 127.0948755),
CourseRoadSnapshot.Node(14081613504L, 1, 37.4126644, 127.0949111),
CourseRoadSnapshot.Node(5850817412L, 2, 37.4126448, 127.0949482),
CourseRoadSnapshot.Node(5850817416L, 2, 37.4126005, 127.0950242),
CourseRoadSnapshot.Node(5850817417L, 2, 37.4125500, 127.0951028),
CourseRoadSnapshot.Node(5850817413L, 2, 37.4124724, 127.0952163),
CourseRoadSnapshot.Node(14081613499L, 1, 37.4122801, 127.0954777),
CourseRoadSnapshot.Node(5850817399L, 2, 37.4121218, 127.0956758),
),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ class LocalPracticeCatalogTest {
assertEquals("36khp37hh8gqcm5nqhe5", route.destination.poiId)
assertEquals(listOf("36khp2hdj2cfcm5nqhe3"), route.waypoints.map { it.poiId })
assertEquals(null, prepared.course.mapPreview)
val road = prepared.course.roadSnapshots.single()
assertEquals("osm-way-619101472-v5", road.id)
assertEquals("창업로40번길", road.title)
assertEquals(76, road.nodes.size)
assertTrue(catalog.courses.filter { it.mapPreview != null }.all { it.navigationRoute == null })
}

Expand Down
2 changes: 2 additions & 0 deletions docs/development/course-map-preview.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@

생활권 코스의 도로선은 장소 대표점을 잇는 선이 아니다. [고정 도로 자료](../../core/database/src/main/kotlin/io/github/hangillab/hangil/core/database/LocalRoadSnapshots.kt)의 way 184443139 버전13과 431903253 버전5를 [버전별 API](https://wiki.openstreetmap.org/wiki/API_v0.6#Version:_GET_/api/0.6/[node|way|relation]/#id/#version)로 읽고, 참조한 노드13개의 버전과 좌표를 개별 버전 응답으로 대조했다. way의 노드 순서를 그대로 보존하며 각 노드 버전도 자료에 포함한다. 이는 way 변경 시각의 역사적 도로 상태나 현재 통행 조건을 확인했다는 뜻은 아니다.

판교를 거쳐 양재까지 코스에서는 내비 요청 지점과 별도로 경유지 인근의 [창업로40번길 공개 도로 원형](https://api.openstreetmap.org/api/0.6/way/619101472/5.json)을 지도와 복습 후보로 표시한다. way 버전 5의 순서와 노드 76개의 개별 버전, 좌표를 공식 API에서 대조했다. 내비 경유지와 목적지 요청 값은 그대로 쓰며 실제 안내 경로의 일치는 MAP-05 실기에서 확인한다.

준비 지도에는 생활권의 세종대로20길과 교차로 코스의 새문안로5길을 각각 연습 예시 구간으로 표시한다. 각 공개 도로의 첫 노드와 마지막 노드가 지도에서 구간 시작과 끝이 된다. 이 지점은 도로선의 범위를 나타내며 정차나 주차 지점으로 사용하지 않는다. 새문안로5길은 [OSM way 66662159 버전 10](https://api.openstreetmap.org/api/0.6/way/66662159/10.json)의 일방통행 노드 순서를 따른다. [서울시 본청 진입 안내](https://www.seoul.go.kr/seoul/map.do)와 [세종문화회관 차량 접근 안내](https://www.sejongpac.or.kr/portal/main/contents.do?menuNo=200390)의 주변 진행 방향을 대조했다. 실제 길안내와 당일 통제는 별도로 확인한다.

[도로 모델](../../core/domain/src/main/kotlin/io/github/hangillab/hangil/core/domain/CourseRoadSnapshot.kt)은 도로 식별자, way 버전, 수집 시각과 ordered node 스냅샷을 보존한다. 지도에는 별도 GeoJSON LineString과 LineLayer로 표시하고, 목록에서 선택한 선만 강조한다. 점과 도로는 한 선택의 서로 다른 target이며 준비 지도에서의 선택은 현재 지도에만 유지된다. 종료 연습의 복습 화면에서는 별도의 사용자 선택 원형을 저장한다. 실제 SDK 표본의 도로 매칭은 연결하지 않았다.
Expand Down
Loading