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 @@ -57,24 +57,14 @@ fun NDGLInputModal(
textAlign: TextAlign = TextAlign.Start,
) {
val focusRequester = remember { FocusRequester() }
var textFieldValue by remember(value) {
var textFieldValue by remember {
mutableStateOf(TextFieldValue(value, selection = TextRange(value.length)))
}
Comment on lines +60 to 62
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd "NDGLInputModal.kt" --type f

Repository: YAPP-Github/27th-App-Team-1-Android

Length of output: 153


🏁 Script executed:

cat -n "core/ui/src/main/java/com/yapp/ndgl/core/ui/designsystem/NDGLInputModal.kt"

Repository: YAPP-Github/27th-App-Team-1-Android

Length of output: 10320


value 외부 변경이 입력창에 반영되지 않아 상태 불일치가 발생합니다.

Line 60에서 textFieldValueremember만으로 초기화되고 외부 value 변경을 감지하는 의존성이 없습니다. 이로 인해 모달이 열린 상태에서 부모 컴포넌트가 value를 변경해도 입력 필드는 이전 값을 유지합니다. 특히 Line 147의 확인 버튼 활성화 조건은 외부 value를 사용하지만 표시되는 텍스트는 textFieldValue이므로, 버튼 상태와 입력 필드 내용이 불일치할 수 있습니다.

🔧 제안 수정
 var textFieldValue by remember {
     mutableStateOf(TextFieldValue(value, selection = TextRange(value.length)))
 }

 LaunchedEffect(Unit) {
     focusRequester.requestFocus()
 }
+
+LaunchedEffect(value) {
+    // 외부 value 변경만 동기화하고, IME 조합 중에는 덮어쓰지 않음
+    if (value != textFieldValue.text && textFieldValue.composition == null) {
+        textFieldValue = textFieldValue.copy(
+            text = value,
+            selection = TextRange(value.length),
+            composition = null,
+        )
+    }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/ui/src/main/java/com/yapp/ndgl/core/ui/designsystem/NDGLInputModal.kt`
around lines 60 - 62, The input modal's internal state textFieldValue (in
NDGLInputModal) is only initialized with remember and doesn't react to external
changes to value, causing UI/state mismatch; fix it by syncing textFieldValue
whenever value changes—for example, keep the existing remember {
mutableStateOf(...) } but add a LaunchedEffect(value) block that sets
textFieldValue = TextFieldValue(value, selection = TextRange(value.length)) (or
use remember(value) to reinitialize), so the displayed text and the external
value used for confirm/enabled logic stay consistent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onValuechange에서 textFieldValue = newValue 하고 있기 때문에 문제가 발생하지는 않았습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mj010504님, 맞습니다! onValueChange에서 사용자 입력은 잘 처리되고 있습니다.

다만 제가 지적한 부분은 사용자 입력이 아닌 외부에서 value prop이 변경될 때의 동기화였습니다. PR 설명에서 한글 자음/모음 결합 문제를 해결하셨다고 하셨는데, 이는 올바른 접근입니다. 한글 IME 조합 중에 textFieldValue를 외부 값으로 덮어쓰면 조합이 깨지는 것이 맞습니다.

한 가지 확인할 점이 있습니다:

  • Line 102의 onValueChangetextFieldValue.text를 전달하지만
  • Line 147의 확인 버튼 활성화 조건은 외부 value를 사용합니다: if (value.isNotEmpty())

현재 구현에서는 모달이 열려있는 동안 외부에서 value를 변경하는 경우가 없다면 문제가 없습니다. 하지만 일관성을 위해 Line 147도 textFieldValue.text.isNotEmpty()를 사용하는 것이 더 안전할 수 있습니다.

모달이 열려있는 동안 외부에서 value를 변경할 가능성이 있나요? 없다면 현재 구현이 적절합니다! 🐰


🧠 Learnings used
Learnt from: jihee-dev
Repo: YAPP-Github/27th-App-Team-1-Android PR: 25
File: feature/travel/src/main/java/com/yapp/ndgl/feature/travel/mytravel/MyTravelContract.kt:52-54
Timestamp: 2026-02-17T22:21:31.141Z
Learning: Use the naming convention estimatedDuration for duration-related fields across Kotlin files to align with server API response parameter names and maintain consistency throughout the codebase. When introducing or renaming duration fields, replace generic names (e.g., duration, totalDuration) with estimatedDuration and update all references (data classes, mappings, JSON parsing) accordingly to avoid mismatch with server payloads.

Learnt from: jihee-dev
Repo: YAPP-Github/27th-App-Team-1-Android PR: 27
File: feature/home/src/main/java/com/yapp/ndgl/feature/home/search/TemplateSearchScreen.kt:0-0
Timestamp: 2026-02-18T18:16:40.700Z
Learning: Use the correct Hilt Compose ViewModel import across Kotlin Android files: import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel (from androidx.hilt:hilt-lifecycle-viewmodel-compose:1.3.0). The old import androidx.hilt.navigation.compose.hiltViewModel is deprecated in Hilt 1.3.0. Ensure the hilt-lifecycle-viewmodel-compose dependency is added to Gradle (both Gradle catalog and module dependencies) and update imports accordingly to avoid runtime issues or build failures.


LaunchedEffect(Unit) {
focusRequester.requestFocus()
}

// value가 변경되면 textFieldValue 업데이트
LaunchedEffect(value) {
if (textFieldValue.text != value) {
textFieldValue = TextFieldValue(
text = value,
selection = TextRange(value.length),
)
}
}

Dialog(onDismissRequest = onDismissRequest) {
Surface(
modifier = modifier.wrapContentHeight(),
Expand Down
6 changes: 4 additions & 2 deletions core/ui/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,12 @@
<string name="start_time_setting">여행 시작 시간 설정</string>
<string name="edit_travel">편집하기</string>
<string name="edit_done">편집 완료</string>
<string name="add_schedule">일정 추가하기</string>
<string name="add_schedule_button_text">일정 추가하기</string>
<string name="select_all">전체 선택</string>
<string name="delete_selected">선택 삭제</string>
<string name="no_schedule_message">아직 %d일차 일정이 없어요</string>
<string name="no_schedule_message">일정이 없어요</string>
<string name="no_schedule_message_detail">아직 %d일차 일정이 없어요</string>
<string name="add_schedule_button_text_no_schedule">%d일차 첫 일정 추가하기</string>

<!-- Dialog - Cancel Edit -->
<string name="cancel_edit_dialog_title">편집 중단</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package com.yapp.ndgl.data.travel.api

import com.yapp.ndgl.data.core.model.BaseResponse
import com.yapp.ndgl.data.travel.model.AddItineraryRequest
import com.yapp.ndgl.data.travel.model.AddItineraryResponse
import com.yapp.ndgl.data.travel.model.BulkUpdateStartTimeRequest
import com.yapp.ndgl.data.travel.model.UpcomingTravelList
import com.yapp.ndgl.data.travel.model.UpcomingTravelResponse
import com.yapp.ndgl.data.travel.model.UpdateItineraryRequest
import com.yapp.ndgl.data.travel.model.UpdateTravelPlaceRequest
import com.yapp.ndgl.data.travel.model.UserTravelTemplateContentInfo
import com.yapp.ndgl.data.travel.model.UserTravelTemplateItinerary
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.PATCH
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
Expand Down Expand Up @@ -41,6 +45,19 @@ interface UserTravelApi {
@Body request: BulkUpdateStartTimeRequest,
): BaseResponse<Unit>

@PATCH("/api/v1/travels/{id}/itinerary/{userTravelPlaceId}")
suspend fun updateTravelPlace(
@Path("id") id: Long,
@Path("userTravelPlaceId") userTravelPlaceId: Long,
@Body request: UpdateTravelPlaceRequest,
): BaseResponse<Unit>

@POST("/api/v1/travels/{id}/itinerary")
suspend fun addItinerary(
@Path("id") id: Long,
@Body request: AddItineraryRequest,
): BaseResponse<AddItineraryResponse>

@PUT("/api/v1/travels/{id}/itinerary")
suspend fun updateItinerary(
@Path("id") id: Long,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.yapp.ndgl.data.travel.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class AddItineraryRequest(
val googlePlaceId: String,
val day: Int,
val sequence: Int,
val startTime: String? = null,
val estimatedDuration: Int,
val memo: String? = null,
@SerialName("budget")
val cost: Int? = null,
val distanceKm: Double? = null,
val transportation: List<TransportationItem>? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.yapp.ndgl.data.travel.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class AddItineraryResponse(
val id: Long,
val day: Int,
val sequence: Int,
val startTime: String? = null,
val estimatedDuration: Int,
val memo: String? = null,
@SerialName("budget")
val cost: Int? = null,
val distanceKm: Double? = null,
val transportation: List<Transportation>? = null,
val travelerTips: List<String>? = null,
val planB: List<PlanBPlace>? = null,
val place: ItineraryPlace,
) {
@Serializable
data class Transportation(
val mode: TransportCategory,
val timeMin: Int,
)

@Serializable
data class PlanBPlace(
val googlePlaceId: String,
val name: String,
val thumbnail: String? = null,
val category: PlaceCategory,
)

@Serializable
data class ItineraryPlace(
val googlePlaceId: String,
val thumbnail: String? = null,
val latitude: Double,
val longitude: Double,
val name: String,
val regularOpeningHours: String? = null,
val googleMapsUri: String? = null,
val category: PlaceCategory,
val priceRange: String? = null,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.yapp.ndgl.data.travel.model

data class ChangePlaceEvent(
val travelId: Long,
val itineraryId: Long,
val day: Int,
val oldGooglePlaceId: String,
val newGooglePlaceId: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.yapp.ndgl.data.travel.model

import kotlinx.serialization.Serializable

@Serializable
data class TransportationItem(
val mode: TransportCategory,
val timeMin: Int,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.yapp.ndgl.data.travel.model

data class TravelCreatedEvent(
val userTravelId: Long,
val templateId: Long,
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,14 @@ data class UpdateItineraryRequest(

@Serializable
data class ItineraryUpdateItem(
val placeId: Long,
val googlePlaceId: String,
val day: Int,
val sequence: Int,
val startTime: String,
val startTime: String? = null,
val estimatedDuration: Int,
@SerialName("travelerTip")
val memo: String?,
val memo: String? = null,
@SerialName("budget")
val cost: Int? = null,
val distanceKm: Double? = null,
val transportation: List<TransportationItem>? = null,
)

@Serializable
data class TransportationItem(
val mode: TransportCategory,
val timeMin: Int,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.yapp.ndgl.data.travel.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class UpdateTravelPlaceRequest(
val memo: String? = null,
@SerialName("budget")
val cost: Int? = null,
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.yapp.ndgl.data.travel.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
Expand All @@ -14,6 +15,8 @@ data class UserTravelTemplateItinerary(
val startTime: String? = null,
val estimatedDuration: Int,
val memo: String? = null,
@SerialName("budget")
val cost: Int? = null,
val distanceKm: Double? = null,
val transportation: List<Transportation>? = null,
val travelerTips: List<String>? = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.yapp.ndgl.data.travel.repository

import com.google.android.gms.maps.model.LatLng
import com.google.android.libraries.places.api.model.AutocompleteSessionToken
import com.google.android.libraries.places.api.model.CircularBounds
import com.google.android.libraries.places.api.net.FindAutocompletePredictionsRequest
import com.google.android.libraries.places.api.net.PlacesClient
import com.yapp.ndgl.data.core.model.error.HttpResponseException
Expand All @@ -23,15 +25,16 @@ class PlaceRepository @Inject constructor(
) {
private var sessionToken: AutocompleteSessionToken? = null

suspend fun searchKeyword(keyword: String, countryCode: String): SearchKeywordResponse {
suspend fun searchKeyword(keyword: String, representativeLatLng: LatLng): SearchKeywordResponse {
if (sessionToken == null) {
sessionToken = AutocompleteSessionToken.newInstance()
}

val bias = CircularBounds.newInstance(representativeLatLng, 10000.0)
val requestBuilder = FindAutocompletePredictionsRequest.builder()
.setQuery(keyword)
.setSessionToken(sessionToken)
.setCountries(countryCode)
.setLocationBias(bias)

val response = placesClient.findAutocompletePredictions(requestBuilder.build()).await()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ package com.yapp.ndgl.data.travel.repository
import com.yapp.ndgl.data.core.model.error.HttpResponseException
import com.yapp.ndgl.data.core.model.getData
import com.yapp.ndgl.data.travel.api.UserTravelApi
import com.yapp.ndgl.data.travel.model.AddItineraryRequest
import com.yapp.ndgl.data.travel.model.AddPlaceEvent
import com.yapp.ndgl.data.travel.model.BulkUpdateStartTimeRequest
import com.yapp.ndgl.data.travel.model.ChangePlaceEvent
import com.yapp.ndgl.data.travel.model.ItineraryUpdateItem
import com.yapp.ndgl.data.travel.model.StartTimeUpdateItem
import com.yapp.ndgl.data.travel.model.TransportationItem
import com.yapp.ndgl.data.travel.model.TravelCreatedEvent
import com.yapp.ndgl.data.travel.model.UpcomingTravelList
import com.yapp.ndgl.data.travel.model.UpcomingTravelResponse
import com.yapp.ndgl.data.travel.model.UpdateItineraryRequest
import com.yapp.ndgl.data.travel.model.UpdateTravelPlaceRequest
import com.yapp.ndgl.data.travel.model.UserTravelTemplateContentInfo
import com.yapp.ndgl.data.travel.model.UserTravelTemplateItinerary
import kotlinx.coroutines.channels.BufferOverflow
Expand All @@ -31,10 +36,32 @@ class UserTravelRepository @Inject constructor(
)
val addPlaceEvent: SharedFlow<AddPlaceEvent> = _addPlaceEvent.asSharedFlow()

private val _travelCreatedEvent = MutableSharedFlow<TravelCreatedEvent>(
replay = 0,
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
val travelCreatedEvent: SharedFlow<TravelCreatedEvent> = _travelCreatedEvent.asSharedFlow()

private val _changePlaceEvent = MutableSharedFlow<ChangePlaceEvent>(
replay = 0,
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
val changePlaceEvent: SharedFlow<ChangePlaceEvent> = _changePlaceEvent.asSharedFlow()

suspend fun emitAddPlaceEvent(event: AddPlaceEvent) {
_addPlaceEvent.emit(event)
}

suspend fun emitTravelCreatedEvent(event: TravelCreatedEvent) {
_travelCreatedEvent.emit(event)
}

suspend fun emitChangePlaceEvent(event: ChangePlaceEvent) {
_changePlaceEvent.emit(event)
}

suspend fun getUpcomingTravel(): UpcomingTravelResponse? {
return try {
userTravelApi.getUpcomingTravel().getData()
Expand All @@ -51,6 +78,24 @@ class UserTravelRepository @Inject constructor(
return userTravelApi.getUpcomingTravelList().getData()
}

suspend fun getUserTravelTemplateItinerary(
travelId: Long,
day: Int,
): UserTravelTemplateItinerary {
return userTravelApi.getUserTravelTemplateItinerary(
id = travelId,
day = day,
).getData()
}

suspend fun getUserTravelTemplateContentInfo(
travelId: Long,
): UserTravelTemplateContentInfo {
return userTravelApi.getUserTravelTemplateContentInfo(
id = travelId,
).getData()
}

suspend fun bulkUpdateStartTime(
travelId: Long,
updates: List<StartTimeUpdateItem>,
Expand All @@ -71,21 +116,45 @@ class UserTravelRepository @Inject constructor(
).getData()
}

suspend fun getUserTravelTemplateItinerary(
suspend fun updateTravelPlace(
travelId: Long,
day: Int,
): UserTravelTemplateItinerary {
return userTravelApi.getUserTravelTemplateItinerary(
userTravelPlaceId: Long,
memo: String? = null,
cost: Int? = null,
) {
userTravelApi.updateTravelPlace(
id = travelId,
day = day,
userTravelPlaceId = userTravelPlaceId,
request = UpdateTravelPlaceRequest(
memo = memo,
cost = cost,
),
).getData()
}

suspend fun getUserTravelTemplateContentInfo(
suspend fun addItinerary(
travelId: Long,
): UserTravelTemplateContentInfo {
return userTravelApi.getUserTravelTemplateContentInfo(
id = travelId,
).getData()
}
googlePlaceId: String,
day: Int,
sequence: Int,
startTime: String? = null,
estimatedDuration: Int,
cost: Int? = null,
memo: String? = null,
distanceKm: Double? = null,
transportation: List<TransportationItem>? = null,
) = userTravelApi.addItinerary(
id = travelId,
request = AddItineraryRequest(
googlePlaceId = googlePlaceId,
day = day,
sequence = sequence,
startTime = startTime,
estimatedDuration = estimatedDuration,
cost = cost,
memo = memo,
distanceKm = distanceKm,
transportation = transportation,
),
).getData()
}
Loading