commit 4eb95571464cab794685715866d55b4580e8955c Author: pjkim Date: Tue Mar 25 15:48:12 2025 +0900 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..44f7c31 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +smart119 \ No newline at end of file diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml new file mode 100644 index 0000000..40a28ab --- /dev/null +++ b/.idea/androidTestResultsUserPreferences.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..7643783 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,123 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..9f4df7e --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..7b3006b --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..148fdd2 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b2c751a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..c64c8f6 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,89 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.example.smart119" + compileSdk = 34 + + signingConfigs { + create("release") { + storeFile = file("D:\\work\\Android\\release\\release.jks") // 키스토어 파일 경로 + storePassword = "rnrmq1!" // 키스토어 비밀번호 + keyAlias = "release_key" // 키 별칭 + keyPassword = "admin123#" // 키 비밀번호 + } + } + + buildFeatures { + buildConfig = true + } + + defaultConfig { + applicationId = "com.example.smart119" + minSdk = 30 + targetSdk = 34 + versionCode = 2 + versionName = "2.1" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + buildConfigField("String", "APP_VERSION", "\"${versionName}\"") + } + + buildTypes { + debug { + isDebuggable = true // ✅ 디버깅 활성화 + } + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + signingConfig = signingConfigs.getByName("release") // 🔹 릴리즈 서명 추가 + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + viewBinding = true + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.navigation.fragment.ktx) + implementation(libs.androidx.navigation.ui.ktx) + implementation(libs.androidx.databinding.runtime) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + // Gson 라이브러리 + implementation("com.google.code.gson:gson:2.8.9") + + // RxJava Core 라이브러리 + implementation("io.reactivex.rxjava3:rxjava:3.1.5") + // RxAndroid (Android-specific extensions) + implementation("io.reactivex.rxjava3:rxandroid:3.0.2") + + // gps, Fused Location Provider API + implementation("com.google.android.gms:play-services-location:21.0.1") + implementation("com.google.android.gms:play-services-location:21.0.1") + + //view pager2 + implementation("androidx.viewpager2:viewpager2:1.1.0") +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm new file mode 100644 index 0000000..f50ea71 Binary files /dev/null and b/app/release/baselineProfiles/0/app-release.dm differ diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm new file mode 100644 index 0000000..ef0f62e Binary files /dev/null and b/app/release/baselineProfiles/1/app-release.dm differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json new file mode 100644 index 0000000..8501720 --- /dev/null +++ b/app/release/output-metadata.json @@ -0,0 +1,37 @@ +{ + "version": 3, + "artifactType": { + "type": "APK", + "kind": "Directory" + }, + "applicationId": "com.example.smart119", + "variantName": "release", + "elements": [ + { + "type": "SINGLE", + "filters": [], + "attributes": [], + "versionCode": 2, + "versionName": "2.1", + "outputFile": "app-release.apk" + } + ], + "elementType": "File", + "baselineProfiles": [ + { + "minApi": 28, + "maxApi": 30, + "baselineProfiles": [ + "baselineProfiles/1/app-release.dm" + ] + }, + { + "minApi": 31, + "maxApi": 2147483647, + "baselineProfiles": [ + "baselineProfiles/0/app-release.dm" + ] + } + ], + "minSdkVersionForDexing": 30 +} \ No newline at end of file diff --git a/app/release/smart119_2_1.apk b/app/release/smart119_2_1.apk new file mode 100644 index 0000000..0f138db Binary files /dev/null and b/app/release/smart119_2_1.apk differ diff --git a/app/src/androidTest/java/com/example/smart119/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/smart119/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..579d972 --- /dev/null +++ b/app/src/androidTest/java/com/example/smart119/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.smart119 + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.smart119", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9afe9b9 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..69b0d01 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/example/smart119/API_Helper.kt b/app/src/main/java/com/example/smart119/API_Helper.kt new file mode 100644 index 0000000..de737ed --- /dev/null +++ b/app/src/main/java/com/example/smart119/API_Helper.kt @@ -0,0 +1,183 @@ +package com.example.smart119 + +import android.os.Handler +import android.os.Looper +import android.widget.Toast +import com.google.gson.Gson +import com.google.gson.JsonObject +import kotlin.math.* + +object API_Helper { + + fun ETA_SEC_TO_STRING(duration:Int):String { + var ret_string = ""; + val minutes = duration / 60 // 분 계산 + val seconds = duration % 60 // 초 계산 + if (duration < 60) { + ret_string = "${seconds}초" + } + else { + ret_string = "${minutes}분 ${seconds}초" + } + + return ret_string + } + + fun DISTANCE_FORMAT_METER_TO_KM(meters: String): String { + val km = meters.toDoubleOrNull()?.div(1000) ?: return "Invalid input" + return String.format("%.2f km", km) + } + + + fun lidar_levelState(level:Int): String { + return when (level) { + -1 -> "정보 없음" + 0 -> "원활" + 1 -> "보통" + 2 -> "약간 혼잡" + 3 -> "혼잡" + 4 -> "매우 혼잡" + else -> "잘못된 레벨" + } + } + fun lidar_levelState_Color(level:String):String { + return when (level) { + "정보 없음" -> "#87CEEB" + "원활" -> "#4CAF50" + "보통" -> "#FFEB3B" + "약간 혼잡" -> "#FF9800" + "혼잡" -> "#F44336" + "매우 혼잡" -> "#B71C1C" + else -> "#000000" // 잘못된 레벨 + } + } + + fun make_findpath_param(org:String, dest:String, summary:Boolean = true):Map { + + return mapOf( + "origin" to "${org}", //126.705098, 37.412087 출발지 경도, 위도 + "destination" to "${dest}", //126.696239, 37.395276 도착지 경도, 위도 + "priority" to "DISTANCE", // 'RECOMMEND' : 추천경로, 'TIME' : 최단시간, 'DISTANCE' : 최단경로 + "car_type" to "1", //자동차 타입 '1' : 소형(예시) 승용차, 16인승 이하 승합차, 2.5 톤 미만 화물차), '2' : 중형, '3' : 대형, '4' : 대형 화물, '5' : 특수 화물, '6' : 경차, '7' : 이륜차 + "car_fuel" to "DIESEL", //연료 종류 + "car_hipass" to "true", // 하이패스 장착 여부 + "summary" to "${summary}", //요약 전달(true 요약정보 제공, false 미제공) + ) + + /* + car_type : [1, + */ + } + + fun make_getAddr_param(lon:String, lat:String):Map { + + return mapOf( + "x" to "${lon}", //126.705098, + "y" to "${lat}" //37.395276 + ) + + /* + car_type : [1, + */ + } + + fun make_addressHosp_param( + Q0:String, // 광역시/도 + Q1:String, // 시군구 + QD:String, // 진료과 + QZ:String, // 병원분류코드 + QN:String, // 관련검색어 + page:String = "1", // 페이지 + row:String = "10", // 결과 개수 + ):JsonObject { + var _param = mapOf( + "Q0" to Q0, + "Q1" to Q1, + "QD" to QD, + "QZ" to QZ, + "QN" to QN, + "page" to page, + "row" to row, + ) + val gson = Gson() + val json = gson.toJsonTree(_param).asJsonObject + return json + } + + fun make_arroundHosp_param( + lon:String, + lat:String, + page: String = "1", + dgids:List = emptyList(), + row: String = "10", + radius: String = "5" + ): JsonObject { + var order = "distance" + var _param = mapOf( + "lon" to lon, //126.696239 경도 + "lat" to lat, //37.395276 위도 + "radius" to radius, // 경/위도 기준 meter 단위 반경내의 병원 + "order" to order, // 정렬 기준 "distance" 고정 + "row" to row, // 결과 row 개수 + "page" to page, // 결과 page 번째 + "dgidIdName" to dgids, // 진료과에 대한 항목들 + ) + val gson = Gson() + val json = gson.toJsonTree(_param).asJsonObject + return json + } + + fun make_arroundHosp_param2( + lon:String, + lat:String, + emogemdv:String = "", // 필수 + prtcode:String = "", // 필수 + emogdesc:String = "", // 필수 아님 + radius: String = "3", + row: String = "10", + page: String = "1", + emergency: String = "Y" + ): JsonObject { + var order = "distance" + var _param = mapOf( + "lon" to lon, //126.696239 경도 + "lat" to lat, //37.395276 위도 + "emogemdv" to emogemdv, // 의료기관번호 + "prtcode" to prtcode, // 진료과코드 + "emogdesc" to emogdesc, // 연관검색어 + "radius" to radius, // 반경(km단위, 숫자만 입력) + "row" to row, // 행 + "page" to page, // 페이지 + "emergency" to emergency // 응급실 여부("Y", "N") + ) + val gson = Gson() + val json = gson.toJsonTree(_param).asJsonObject + return json + } + + fun make_detail_hosp_param( + hpid: String + ) :JsonObject { + var _param = mapOf( + "hpid" to hpid, // 병원 아이디 + ) + val gson = Gson() + val json = gson.toJsonTree(_param).asJsonObject + return json + } + + fun calculateDistance(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double { + val R = 6371.0 // 지구 반지름 (단위: km) + + val dLat = Math.toRadians(lat2 - lat1) + val dLon = Math.toRadians(lon2 - lon1) + + val a = sin(dLat / 2) * sin(dLat / 2) + + cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) * + sin(dLon / 2) * sin(dLon / 2) + + val c = 2 * atan2(sqrt(a), sqrt(1 - a)) + + return R * c // 거리 반환 (단위: km) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/AlertDialogUtil.kt b/app/src/main/java/com/example/smart119/AlertDialogUtil.kt new file mode 100644 index 0000000..0caf8e5 --- /dev/null +++ b/app/src/main/java/com/example/smart119/AlertDialogUtil.kt @@ -0,0 +1,138 @@ +package com.example.smart119 + +import android.app.Activity +import android.content.Context +import android.text.SpannableStringBuilder +import android.util.TypedValue +import android.widget.TextView +import androidx.appcompat.app.AlertDialog + +object AlertDialogUtil { + // 기본 AlertDialog 생성 함수 + fun showAlert( + context: Context, + title: String, + message: String, + positiveText: String = "확인", + negativeText: String = "취소", + onPositiveClick: (() -> Unit)? = null, + onNegativeClick: (() -> Unit)? = null + ) { + val builder = AlertDialog.Builder(context) + builder.setTitle(title) +// builder.setMessage(message) + + val messageTextView = TextView(context).apply { + text = message + setPadding(40, 40, 40, 0) + textSize = 16f + setLineSpacing(10f, 1.2f) // 첫 번째 인자는 추가 간격(px), 두 번째 인자는 배수 + } + builder.setView(messageTextView) + + // Positive 버튼 설정 + builder.setPositiveButton(positiveText) { dialog, _ -> + onPositiveClick?.invoke() // Positive 버튼 클릭 동작 + dialog.dismiss() + } + + // Negative 버튼 설정 + builder.setNegativeButton(negativeText) { dialog, _ -> + onNegativeClick?.invoke() // Negative 버튼 클릭 동작 + dialog.dismiss() + } + + // 팝업 표시 + val dialog = builder.create() + dialog.show() + } + + fun showAlert_OK( + context: Context, + title: String, + message: String, + positiveText: String = "확인", + negativeText: String = "취소", + onPositiveClick: (() -> Unit)? = null +// onNegativeClick: (() -> Unit)? = null + ) { + val builder = AlertDialog.Builder(context) + builder.setTitle(title) +// builder.setMessage(message) + + val messageTextView = TextView(context).apply { + text = message + setPadding(40, 40, 40, 0) + textSize = 16f + setLineSpacing(10f, 1.2f) // 첫 번째 인자는 추가 간격(px), 두 번째 인자는 배수 + } + builder.setView(messageTextView) + + // Positive 버튼 설정 + builder.setPositiveButton(positiveText) { dialog, _ -> + onPositiveClick?.invoke() // Positive 버튼 클릭 동작 + dialog.dismiss() + } + + // Negative 버튼 설정 +// builder.setNegativeButton(negativeText) { dialog, _ -> +// onNegativeClick?.invoke() // Negative 버튼 클릭 동작 +// dialog.dismiss() +// } + + // 팝업 표시 + val dialog = builder.create() + dialog.show() + } + + fun showAlert_CUSTOM( + context: Context, + title: String, + sppStr: SpannableStringBuilder, + positiveText: String = "확인", + negativeText: String = "취소", + onPositiveClick: (() -> Unit)? = null, + onNegativeClick: (() -> Unit)? = null + ) { + val builder = AlertDialog.Builder(context) + builder.setTitle(title) + /* + val spannableMessage = SpannableStringBuilder().apply { + val boldText = "확인" + append(boldText) + setSpan(StyleSpan(Typeface.BOLD), 0, boldText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + append(" 선택시 환자정보가 전송됩니다.") + } + */ + + val textView = TextView(context).apply { + text = sppStr + setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f) + setLineSpacing(12f, 1.3f) // 줄간 간격 조절 (추가간격 px, 배수) + setPadding(40, 40, 40, 0) // 내부 여백도 함께 설정 + } + +// builder.setMessage(sppStr) + builder.setView(textView) + + // Positive 버튼 설정 + builder.setPositiveButton(positiveText) { dialog, _ -> + onPositiveClick?.invoke() // Positive 버튼 클릭 동작 + dialog.dismiss() + } + + // Negative 버튼 설정 + builder.setNegativeButton(negativeText) { dialog, _ -> + onNegativeClick?.invoke() // Negative 버튼 클릭 동작 + dialog.dismiss() + } + + // 팝업 표시 + val dialog = builder.create() + dialog.show() + } + + fun getActivityContext(): Activity? { + return GlobalActivityTracker.currentActivity + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/App.kt b/app/src/main/java/com/example/smart119/App.kt new file mode 100644 index 0000000..de0839f --- /dev/null +++ b/app/src/main/java/com/example/smart119/App.kt @@ -0,0 +1,18 @@ +package com.example.smart119 + +import android.app.Application +import android.util.Log + +class App : Application() { + companion object { + lateinit var instance: App + private set + } + + override fun onCreate() { + super.onCreate() + Log.d("App", "App.onCreate 실행됨") + registerActivityLifecycleCallbacks(GlobalActivityTracker) + instance = this + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/GlobalActivityTracker.kt b/app/src/main/java/com/example/smart119/GlobalActivityTracker.kt new file mode 100644 index 0000000..890905b --- /dev/null +++ b/app/src/main/java/com/example/smart119/GlobalActivityTracker.kt @@ -0,0 +1,27 @@ +package com.example.smart119 + +import android.app.Activity +import android.app.Application +import android.os.Bundle +import android.util.Log + +object GlobalActivityTracker : Application.ActivityLifecycleCallbacks { + var currentActivity: Activity? = null + + override fun onActivityResumed(activity: Activity) { + currentActivity = activity + Log.d("GlobalActivityTracker", "현재 액티비티: ${activity.localClassName}") + } + + override fun onActivityPaused(activity: Activity) { + if (currentActivity === activity) { + currentActivity = null + } + } + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {} + override fun onActivityStarted(activity: Activity) {} + override fun onActivityStopped(activity: Activity) {} + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + override fun onActivityDestroyed(activity: Activity) {} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/HelpUtility.kt b/app/src/main/java/com/example/smart119/HelpUtility.kt new file mode 100644 index 0000000..224d477 --- /dev/null +++ b/app/src/main/java/com/example/smart119/HelpUtility.kt @@ -0,0 +1,14 @@ +package com.example.smart119 + +import android.os.Handler +import android.os.Looper + +object HelpUtility { + private val delayHandler = Handler(Looper.getMainLooper()) + + fun delay_executor(delayMillis: Long = 1000, callback: () -> Unit) { + delayHandler.postDelayed({ + callback() + }, delayMillis) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/ListItem.kt b/app/src/main/java/com/example/smart119/ListItem.kt new file mode 100644 index 0000000..4b9af6b --- /dev/null +++ b/app/src/main/java/com/example/smart119/ListItem.kt @@ -0,0 +1,6 @@ +package com.example.smart119 + +sealed class ListItem { + data class Header(val columns: List) : ListItem() + data class Row(val values: List) : ListItem() +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/LocationService.kt b/app/src/main/java/com/example/smart119/LocationService.kt new file mode 100644 index 0000000..30059e2 --- /dev/null +++ b/app/src/main/java/com/example/smart119/LocationService.kt @@ -0,0 +1,99 @@ +package com.example.smart119 + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Intent +import android.content.pm.PackageManager +import android.location.Location +import android.os.IBinder +import android.os.Looper +import android.util.Log +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationCallback +import com.google.android.gms.location.LocationRequest +import com.google.android.gms.location.LocationResult +import com.google.android.gms.location.LocationServices +import com.google.android.gms.location.Priority + +class LocationService : Service() { + private lateinit var fusedLocationClient: FusedLocationProviderClient + private lateinit var locationCallback: LocationCallback + + override fun onCreate() { + super.onCreate() + + fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) + + locationCallback = object : LocationCallback() { + override fun onLocationResult(result: LocationResult) { + for (location: Location in result.locations) { +// Log.d("LocationService", "위치: ${location.latitude}, ${location.longitude}") + // TODO: 서버 전송 또는 처리 로직 추가 + val lat = location.latitude + val lon = location.longitude + + // ✅ LocationHelper에 위치 저장 + LocationHelper.setCurrentLocation(lat, lon) + println("[GPS_UPDATE] lon:${lon}, lat:${lat}") + + // ✅ Broadcast도 필요 시 같이 전송 +// broadcastLocation(lat, lon) + } + } + } + + startLocationUpdates() + } + + private fun startLocationUpdates() { + val request = LocationRequest.create().apply { + interval = 5_000 + fastestInterval = 3_000 + priority = Priority.PRIORITY_HIGH_ACCURACY + } + + if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED + ) return + + fusedLocationClient.requestLocationUpdates(request, locationCallback, Looper.getMainLooper()) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + startForegroundServiceWithNotification() + return START_STICKY + } + + private fun startForegroundServiceWithNotification() { + val channelId = "location_service_channel" + val channel = NotificationChannel( + channelId, + "Location Service", + NotificationManager.IMPORTANCE_LOW + ) + val manager = getSystemService(NotificationManager::class.java) + manager.createNotificationChannel(channel) + + val notification = NotificationCompat.Builder(this, channelId) + .setContentTitle("위치 추적 중") + .setContentText("백그라운드에서 위치를 추적하고 있습니다.") + .setSmallIcon(R.drawable.ic_location_on) + .build() + + try { + startForeground(1, notification) + } catch (e: Exception) { + println(e.message) + } + } + + override fun onDestroy() { + super.onDestroy() + fusedLocationClient.removeLocationUpdates(locationCallback) + } + + override fun onBind(intent: Intent?): IBinder? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/LocationUtils.kt b/app/src/main/java/com/example/smart119/LocationUtils.kt new file mode 100644 index 0000000..7f142ad --- /dev/null +++ b/app/src/main/java/com/example/smart119/LocationUtils.kt @@ -0,0 +1,179 @@ +package com.example.smart119 + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.location.LocationManager +import android.net.Uri +import android.provider.Settings +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices +import com.google.gson.JsonObject + +object LocationHelper { + private lateinit var fusedLocationClient: FusedLocationProviderClient + private val LOCATION_PERMISSION_REQUEST_CODE = 100 + + // ✅ 실시간 위치 저장용 변수 + private var currentLat: Double = 0.0 + private var currentLon: Double = 0.0 + + + + fun startLocationService(ctx: Context, onFinish: () -> Unit = {}) { + if (!checkAndRequestLocationPermission(ctx, onFinish)) return + + if (!isGpsEnabled(ctx)) { + promptEnableGps(ctx) + return + } + + val serviceIntent = Intent(ctx, LocationService::class.java) + ContextCompat.startForegroundService(ctx, serviceIntent) + println("[startLocationService] - start") + } + fun stopLocationService(ctx: Context) { + val serviceIntent = Intent(ctx, LocationService::class.java) + ctx.stopService(serviceIntent) + } + + fun setCurrentLocation(lat: Double, lon: Double) { + currentLat = lat + currentLon = lon + } + + fun getCurrentLat(): Double = currentLat + fun getCurrentLon(): Double = currentLon + + fun init( + ctx: Context, + onFinish:() -> Unit + ) { + fusedLocationClient = LocationServices.getFusedLocationProviderClient(ctx) + if (!checkAndRequestLocationPermission(ctx, onFinish)) { + return + } + + handleGpsState(ctx) + } + + fun handleGpsState(ctx: Context) { + if (isGpsEnabled(ctx)) { + //Toast.makeText(this, "GPS is already enabled", Toast.LENGTH_SHORT).show() + println("GPS Enabled") + startLocationService(ctx) + } else { + println("GPS not Enabled") + promptEnableGps(ctx) + } + } + +// fun permissionResultInvoke( +// ctx: Context, +// requestCode: Int, +// permissions: Array, +// grantResults: IntArray, +// onFinish: () -> Unit +// ) { +// if (requestCode == LOCATION_PERMISSION_REQUEST_CODE) { +// if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { +// LocationHelper.handleGpsState(ctx) +// } else { +// // "다시 묻지 않음" 여부 확인 +// if (!ActivityCompat.shouldShowRequestPermissionRationale( +// ctx as Activity, Manifest.permission.ACCESS_FINE_LOCATION +// ) +// ) { +// // 설정 화면으로 이동 안내(MainActivity에 구현) +// showSettingsDialog(ctx, onFinish) +// } else { +// Toast.makeText(ctx, "권한이 거부되었습니다. 앱을 종료합니다.", Toast.LENGTH_SHORT).show() +// onFinish(); +// } +// } +// } +// } + + private fun showSettingsDialog(ctx: Context, onFinish: () -> Unit) { + AlertDialog.Builder(ctx) + .setTitle("권한 필요") + .setMessage("위치 권한이 필요합니다. 설정에서 권한을 허용해주세요.") + .setPositiveButton("설정으로 이동") { _, _ -> + goToAppSettings(ctx) + } + .setNegativeButton("취소") { _, _ -> + Toast.makeText(ctx, "권한이 거부되었습니다. 앱을 종료합니다.", Toast.LENGTH_SHORT).show() + onFinish() + } + .show() + } + private fun goToAppSettings(context: Context) { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", context.packageName, null) + } + context.startActivity(intent) + } + + //1.현재 gps 상태 확인 + private fun isGpsEnabled(ctx: Context): Boolean { + val locationManager = ctx.getSystemService(Context.LOCATION_SERVICE) as LocationManager + return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) + } + + //2.gps 활성화 요청 + private fun promptEnableGps(ctx: Context) { + val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) + ctx.startActivity(intent) + } + + //3. 권한 요청 코드 + private fun checkAndRequestLocationPermission(ctx: Context, onFinish: () -> Unit): Boolean { + if (ContextCompat.checkSelfPermission(ctx, Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED + ) { + if (ActivityCompat.shouldShowRequestPermissionRationale( + ctx as Activity, + Manifest.permission.ACCESS_FINE_LOCATION + ) + ) { + // 권한 요청 설명 + showPermissionRationale(ctx, onFinish) + } else { + // 권한 요청 + ActivityCompat.requestPermissions( + ctx, + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), + LOCATION_PERMISSION_REQUEST_CODE + ) + } + return false + } + return true + } + + private fun showPermissionRationale(ctx: Context, onFinish: () -> Unit) { + AlertDialog.Builder(ctx) + .setTitle("위치 권한 필요") + .setMessage("앱에서 정확한 위치 서비스를 제공하려면 위치 권한이 필요합니다.") + .setPositiveButton("권한 허용") { _, _ -> + ActivityCompat.requestPermissions( + ctx as Activity, + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), + LOCATION_PERMISSION_REQUEST_CODE + ) + } + .setNegativeButton("취소") { _, _ -> + Toast.makeText(ctx, "권한이 거부되었습니다. 앱을 종료합니다.", Toast.LENGTH_SHORT).show() + onFinish() + } + .show() + } + +} diff --git a/app/src/main/java/com/example/smart119/LoginManager.kt b/app/src/main/java/com/example/smart119/LoginManager.kt new file mode 100644 index 0000000..e9eefb1 --- /dev/null +++ b/app/src/main/java/com/example/smart119/LoginManager.kt @@ -0,0 +1,32 @@ +package com.example.smart119 + +import com.google.gson.JsonObject + +object LoginManager { + private var isLoggedIn: Boolean = false // 기본값 설정 + var tryLoginBody: JsonObject = JsonObject().apply { + addProperty("userId", "") + addProperty("carNo", "") + addProperty("ver", "") + } + + // 로그인 상태 확인 + fun isLoggedIn(): Boolean { + return isLoggedIn + } + + // 로그인 상태 업데이트 + fun setLoggedIn(status: Boolean) { + isLoggedIn = status + } + + fun getLoggedInfo() :JsonObject{ + return tryLoginBody; + } + + fun setLoggedInfo(userId:String, carNo:String, ver:String) { + tryLoginBody.addProperty("userId", userId) + tryLoginBody.addProperty("carNo", carNo) + tryLoginBody.addProperty("ver", ver) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/MainActivity.kt b/app/src/main/java/com/example/smart119/MainActivity.kt new file mode 100644 index 0000000..a31bb8b --- /dev/null +++ b/app/src/main/java/com/example/smart119/MainActivity.kt @@ -0,0 +1,335 @@ +package com.example.smart119 + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Bundle +import android.provider.Settings +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.findNavController +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.navigateUp +import android.view.Menu +import android.view.MenuItem +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.appcompat.app.AlertDialog +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.viewModels +import com.example.smart119.databinding.ActivityMainBinding +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.setupWithNavController +import com.example.smart119.datas.HospitalSearch +import com.example.smart119.datas.SaveLogin +import com.example.smart119.interfaces.RetrofitClient +import com.example.smart119.viewModel.TransferViewModel +import com.example.smart119.views.LoadingDialog +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.material.bottomnavigation.BottomNavigationView +import com.google.android.material.textfield.TextInputEditText +import com.google.gson.JsonObject +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class MainActivity : AppCompatActivity() { + + private lateinit var appBarConfiguration: AppBarConfiguration + private lateinit var binding: ActivityMainBinding + private lateinit var fusedLocationClient: FusedLocationProviderClient + private val LOCATION_PERMISSION_REQUEST_CODE = 100 + private val viewModel by viewModels() + + private val requestPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + // 권한이 허용되면 서비스 시작 + LocationHelper.startLocationService(this) + } else { + // 권한 거부됨 -> 필요한 안내 처리 + } + } + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + +// binding = DataBindingUtil.setContentView(this, R.layout.activity_main) + +// fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) + + // 권한 요청 흐름 시작 +// if (!checkAndRequestLocationPermission()) { +// return +// } + +// LocationHelper.init(this, { +// finish() +// }); + LocationHelper.startLocationService(this) + + SaveLogin.init(this); + HospitalSearch.init_hospital_req(this) + + +// LocationHelper.getLastKnownLocation_TOASTMSG(this) + + val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment + val navController = navHostFragment.navController + + val bottomNavigationView = findViewById(R.id.bottom_navigation) + bottomNavigationView.setupWithNavController(navController) + + //이송 초기화 + TransferManager.setIdle() + + //네비게이션하고 fragment 연결 + bottomNavigationView.setOnItemSelectedListener { item -> + when (item.itemId) { + R.id.emgLogin -> { + if (LoginManager.isLoggedIn()) { + val _inputId = SaveLogin.getUserId() + val _inputCarno = SaveLogin.getCarNo() + val message = """ + 현재 로그인된 정보입니다. + 아이디: $_inputId + 차량번호: $_inputCarno + 로그아웃하시겠습니까? + + """.trimIndent() + + AlertDialogUtil.showAlert( + context = this, + title = "로그인 상태 확인", + message = message, + onPositiveClick = { + //로그아웃 처리 + println("logout") + navController.navigate(R.id.LoginFragment) + SaveLogin.setLogin(false) + TransferManager.clear_state() + }, + onNegativeClick = { + + } + ) + false + } + else { + navController.navigate(R.id.LoginFragment) + true + } + } + R.id.hospSearch -> { + navController.navigate(R.id.HospSearchFragment) + true + } + R.id.hospTransfer -> { + navController.navigate(R.id.TransferFragment) + true + } + else -> false + } + } + + TransferManager.registerListener("transfer_update_event", ::TransferUpdateEvent) + TransferManager.registerListener("transfer_dialogmsg_event", ::TransferDialogMsgEvent) + + MedicalInfo.init() + +// viewModel.requestData() +// viewModel.someLiveData.observe(viewLifecycleOwner) { result -> +// // UI 업데이트 +// } + } + override fun onCreateOptionsMenu(menu: Menu): Boolean { + // Inflate the menu; this adds items to the action bar if it is present. + menuInflater.inflate(R.menu.menu_main, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + return when (item.itemId) { + R.id.action_settings -> true + else -> super.onOptionsItemSelected(item) + } + } + + override fun onSupportNavigateUp(): Boolean { + val navController = findNavController(R.id.nav_host_fragment_content_main) + return navController.navigateUp(appBarConfiguration) + || super.onSupportNavigateUp() + } + + //권한 확인 + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + + if (requestCode == LOCATION_PERMISSION_REQUEST_CODE) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + LocationHelper.handleGpsState(this) + } else { + // "다시 묻지 않음" 여부 확인 + if (!ActivityCompat.shouldShowRequestPermissionRationale( + this as Activity, Manifest.permission.ACCESS_FINE_LOCATION + ) + ) { + // 설정 화면으로 이동 안내(MainActivity에 구현) + showSettingsDialog(this@MainActivity) { finish() } + } else { + Toast.makeText(this, "권한이 거부되었습니다. 앱을 종료합니다.", Toast.LENGTH_SHORT).show() + finish(); + } + } + } + else { + println("gps 권한 여부 코드 : ${requestCode}") + } + } + + private fun showSettingsDialog(activity: Activity, onFinish: () -> Unit) { + AlertDialog.Builder(activity) + .setTitle("권한 필요") + .setMessage("위치 권한이 필요합니다. 설정에서 권한을 허용해주세요.") + .setPositiveButton("설정으로 이동") { _, _ -> + goToAppSettings(activity) + } + .setNegativeButton("취소") { _, _ -> + Toast.makeText(activity, "권한이 거부되었습니다. 앱을 종료합니다.", Toast.LENGTH_SHORT).show() + onFinish() + } + .show() + } + + private fun goToAppSettings(activity: Activity) { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", activity.packageName, null) + } + activity.startActivity(intent) + } + + private fun onAccessButtonClick() { + Toast.makeText(this, "Button clicked!", Toast.LENGTH_SHORT).show() + } + + + fun TransferUpdateEvent(data: JsonObject?) { + } + + fun TransferDialogMsgEvent(data: JsonObject?) { + try { + var _msgObj = data?.asJsonObject!!; + + AlertDialogUtil.showAlert( + context = this, + title = _msgObj["title"].toString().trim('"'), + message = _msgObj["msg"].toString().trim('"'), + onPositiveClick = { + val activity = AlertDialogUtil.getActivityContext() + val bottomNav = activity?.findViewById(R.id.bottom_navigation) + if (bottomNav != null && + bottomNav.selectedItemId != R.id.hospSearch + ){ + bottomNav.selectedItemId = R.id.hospSearch + } + }, + onNegativeClick = { + val activity = AlertDialogUtil.getActivityContext() + val bottomNav = activity?.findViewById(R.id.bottom_navigation) + if (bottomNav != null && + bottomNav.selectedItemId != R.id.hospSearch + ) { + bottomNav.selectedItemId = R.id.hospSearch + } + } + ) + } catch (e: Exception) { + //TODO("Not yet implemented") + } + } + + //3. 권한 요청 코드 + private fun checkAndRequestLocationPermission(): Boolean { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED + ) { + if (ActivityCompat.shouldShowRequestPermissionRationale( + this, + Manifest.permission.ACCESS_FINE_LOCATION + ) + ) { + // 권한 요청 설명 + showPermissionRationale() + } else { + // 권한 요청 + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), + LOCATION_PERMISSION_REQUEST_CODE + ) + } + return false + } + return true + } + + private fun showPermissionRationale() { + AlertDialog.Builder(this) + .setTitle("위치 권한 필요") + .setMessage("앱에서 정확한 위치 서비스를 제공하려면 위치 권한이 필요합니다.") + .setPositiveButton("권한 허용") { _, _ -> + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), + LOCATION_PERMISSION_REQUEST_CODE + ) + } + .setNegativeButton("취소") { _, _ -> + Toast.makeText(this, "권한이 거부되었습니다. 앱을 종료합니다.", Toast.LENGTH_SHORT).show() + finish() + } + .show() + } + + private fun showSettingsDialog() { + AlertDialog.Builder(this) + .setTitle("권한 필요") + .setMessage("위치 권한이 필요합니다. 설정에서 권한을 허용해주세요.") + .setPositiveButton("설정으로 이동") { _, _ -> + goToAppSettings(this) + } + .setNegativeButton("취소") { _, _ -> + Toast.makeText(this, "권한이 거부되었습니다. 앱을 종료합니다.", Toast.LENGTH_SHORT).show() + finish() + } + .show() + } + + //4. 설정 화면으로 이동 + fun goToAppSettings(context: Context) { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", context.packageName, null) + } + context.startActivity(intent) + } + + override fun onDestroy() { + super.onDestroy() + LocationHelper.stopLocationService(this) + TransferManager.clear_state(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/MedicalDepartment.kt b/app/src/main/java/com/example/smart119/MedicalDepartment.kt new file mode 100644 index 0000000..b416663 --- /dev/null +++ b/app/src/main/java/com/example/smart119/MedicalDepartment.kt @@ -0,0 +1,218 @@ +package com.example.smart119 + +object MedicalInfo { + private val prtcodeMap = mutableMapOf() // 진료과 목록용 맵 + private val emogemdvMap = mutableMapOf() // 의료기관 목록용 맵 + private val addressQZMap = mutableMapOf() // QZ 분류용 맵 + private val prtGroupMap = mutableMapOf>() // 진료과 리스트 맵 + + fun init() { + prtcode_init() + emogemdv_init() + address_qz_init(); + prtGroupMap_init(); + } + private fun prtcode_init() { + prtcodeMap.put("가정의학과", "D022") + prtcodeMap.put("결핵과", "D015") + prtcodeMap.put("구강내과", "D040") + prtcodeMap.put("구강병리과", "D042") + prtcodeMap.put("구강악안면방사선과", "D041") + prtcodeMap.put("구강악안면외과", "D034") + prtcodeMap.put("구강안면외과", "D054") + prtcodeMap.put("내과", "D001") + prtcodeMap.put("마취통증의학과", "D017") + prtcodeMap.put("방사선종양학과", "D031") + prtcodeMap.put("병리과", "D032") + prtcodeMap.put("비뇨의학과", "D014") + prtcodeMap.put("사상체질과", "D050") + prtcodeMap.put("산부인과", "D011") + prtcodeMap.put("산업의학과", "D025") + prtcodeMap.put("성형외과", "D010") + prtcodeMap.put("소아청소년과", "D002") + prtcodeMap.put("소아치과", "D037") + prtcodeMap.put("신경과", "D003") + prtcodeMap.put("신경외과", "D009") + prtcodeMap.put("심장혈관흉부외과", "D007") + prtcodeMap.put("안과", "D012") + prtcodeMap.put("영상의학과", "D018") + prtcodeMap.put("영상치의학과", "D055") + prtcodeMap.put("예방의학과", "D029") + prtcodeMap.put("예방치과", "D043") + prtcodeMap.put("외과", "D006") + prtcodeMap.put("응급의학과", "D024") + prtcodeMap.put("이비인후과", "D013") + prtcodeMap.put("작업환경의학과", "D053") + prtcodeMap.put("재활의학과", "D016") + prtcodeMap.put("정신건강의학과", "D004") + prtcodeMap.put("정형외과", "D008") + prtcodeMap.put("진단검사의학과", "D033") + prtcodeMap.put("치과", "D026") + prtcodeMap.put("치과교정과", "D036") + prtcodeMap.put("치과보존과", "D039") + prtcodeMap.put("치과보철과", "D035") + prtcodeMap.put("치료방사선과", "D019") + prtcodeMap.put("치주과", "D038") + prtcodeMap.put("침구과", "D051") + prtcodeMap.put("통합치의학과", "D056") + prtcodeMap.put("피부과", "D005") + prtcodeMap.put("한방내과", "D044") + prtcodeMap.put("한방부인과", "D045") + prtcodeMap.put("한방소아과", "D046") + prtcodeMap.put("한방신경정신과", "D048") + prtcodeMap.put("한방안이비인후피부과", "D047") + prtcodeMap.put("한방응급과", "D052") + prtcodeMap.put("한방재활의학과", "D049") + prtcodeMap.put("해부병리과", "D021") + prtcodeMap.put("핵의학과", "D023") + } + private fun emogemdv_init() { + emogemdvMap.put("상급종합병원", "001") + emogemdvMap.put("종합병원", "011") + emogemdvMap.put("병원", "021") + emogemdvMap.put("요양병원", "028") + emogemdvMap.put("의원", "031") + emogemdvMap.put("치과병원", "041") + emogemdvMap.put("치과의원", "051") + emogemdvMap.put("보건소", "071") + emogemdvMap.put("보건지소", "072") + emogemdvMap.put("보건진료소", "073") + emogemdvMap.put("보건의료원", "075") + emogemdvMap.put("한방병원", "092") + emogemdvMap.put("한의원", "093") + } + private fun address_qz_init() { // 주소검색 시 병원 분류 코드 + addressQZMap.put("종합병원", "A") + addressQZMap.put("병원", "B") + addressQZMap.put("의원", "C") + addressQZMap.put("요양병원", "D") + addressQZMap.put("한방병원", "E") + addressQZMap.put("한의원", "G") + addressQZMap.put("기타", "I") + addressQZMap.put("치과병원", "M") + addressQZMap.put("치과의원", "N") + addressQZMap.put("보건소", "R") + addressQZMap.put("기타(구급차)", "W") + } + + private fun prtGroupMap_init() { + prtGroupMap.put( + "🚨 응급 및 외상 치료", + mutableListOf("응급의학과", "외과", "신경외과", "심장혈관흉부외과") + ) + + prtGroupMap.put( + "🏥 내과 및 심장 관련", + mutableListOf("내과", "심장내과", "결핵과", "예방의학과", "산업의학과", "작업환경의학과") + ) + + prtGroupMap.put( + "🦴 골절 및 재활", + mutableListOf("정형외과", "재활의학과", "한방재활의학과") + ) + + prtGroupMap.put( + "💊 마취 및 통증 관리", + mutableListOf("마취통증의학과") + ) + + prtGroupMap.put( + "👶 소아 및 여성 건강", + mutableListOf("소아청소년과", "산부인과", "한방소아과", "한방부인과") + ) + + prtGroupMap.put( + "🦷 치과 계열", + mutableListOf( + "치과", "구강내과", "구강병리과", "구강악안면방사선과", "구강악안면외과", + "소아치과", "치과교정과", "치과보존과", "치과보철과", "치주과", + "영상치의학과", "예방치과", "통합치의학과" + ) + ) + + prtGroupMap.put( + "🧠 신경 및 정신 건강", + mutableListOf("신경과", "정신건강의학과", "한방신경정신과") + ) + + prtGroupMap.put( + "👂 이비인후과 및 안과", + mutableListOf("이비인후과", "안과", "한방안이비인후피부과") + ) + + prtGroupMap.put( + "📡 방사선 및 영상의학", + mutableListOf("영상의학과", "치료방사선과", "방사선종양학과", "핵의학과") + ) + + prtGroupMap.put( + "🩺 병리 및 진단의학", + mutableListOf("병리과", "해부병리과", "진단검사의학과") + ) + + prtGroupMap.put( + "🩸 비뇨 및 피부과", + mutableListOf("비뇨의학과", "피부과") + ) + + prtGroupMap.put( + "🌿 한방 계열", + mutableListOf( + "한방내과", "한방부인과", "한방소아과", "한방신경정신과", "한방안이비인후피부과", + "한방응급과", "한방재활의학과", "침구과", "사상체질과" + ) + ) + } + + fun get_prt_code(prtName:String):String? { + return prtcodeMap.get(prtName) + } + fun get_emogemdv_code(emogemdvName:String):String? { + return emogemdvMap.get(emogemdvName) + } + fun get_addressQZ_code(qzName:String):String? { + return addressQZMap.get(qzName) + } + + fun get_prt_list():MutableList { + var returnList:MutableList = mutableListOf() + prtcodeMap.forEach { key, value -> + returnList.add(key) + } + return returnList + } + fun get_emogemdv_list():MutableList { + var returnList:MutableList = mutableListOf() + emogemdvMap.forEach { key, value -> + returnList.add(key) + } + return returnList + } + fun get_addressQZ_list():MutableList { + var returnList:MutableList = mutableListOf() + addressQZMap.forEach { key, value -> + returnList.add(key) + } + return returnList + } + + fun get_prt_group_map():MutableMap> { + return prtGroupMap; + } + + /* //region 사용 예 + val prtCode = MedicalInfo.get_prt_code("신경외과") + val emoCode = MedicalInfo.get_emogemdv_code("병원") + println("신경외과:${prtCode}, 병원:${emoCode}") + + val getPRTList = MedicalInfo.get_prt_list() + getPRTList.forEach { prtName -> + println(prtName) + } + println("aaaaaaaaaaaaaaaa") + val getEMOList = MedicalInfo.get_emogemdv_list() + getEMOList.forEach { emoName -> + println(emoName) + } + */ // endregion +} diff --git a/app/src/main/java/com/example/smart119/MyCookieJar.kt b/app/src/main/java/com/example/smart119/MyCookieJar.kt new file mode 100644 index 0000000..8fd98d8 --- /dev/null +++ b/app/src/main/java/com/example/smart119/MyCookieJar.kt @@ -0,0 +1,52 @@ +package com.example.smart119 +import okhttp3.Cookie +import okhttp3.CookieJar +import okhttp3.HttpUrl + +object MyCookieJar : CookieJar { + + private val cookieStore: MutableMap> = mutableMapOf() + private val cookieCustom: MutableMap = mutableMapOf() + + override fun saveFromResponse(url: HttpUrl, cookies: List) { + val host = url.url().host // URL의 호스트 이름 추출 + if (cookies.isNotEmpty()) { + cookieStore[host] = cookies.toMutableList() + } + } + + override fun loadForRequest(url: HttpUrl): List { + val host = url.url().host + return cookieStore[host] ?: mutableListOf() + } + fun setCookie(cookie:String) { + var cookieSplits = cookie.split("=") +// println(cookieSplits[0]) +// println(cookieSplits[1]) + cookieCustom.put(cookieSplits[0], cookie) + } + fun getCustomCookies():MutableList { + var retList:MutableList = mutableListOf() + cookieCustom.forEach { key, value -> + retList.add(value) + } + + return retList; + } + fun getAllCookies():List { + return cookieStore.values.flatten() + } + + + // 쿠키 상태 확인 (디버깅용) + fun getCookies(): Map> { + return cookieStore + } + + // 모든 쿠키 삭제 + fun clearCookies() { + cookieStore.clear() + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/RandomNumberUtil.kt b/app/src/main/java/com/example/smart119/RandomNumberUtil.kt new file mode 100644 index 0000000..f3accdf --- /dev/null +++ b/app/src/main/java/com/example/smart119/RandomNumberUtil.kt @@ -0,0 +1,40 @@ +package com.example.smart119 + +import kotlin.random.Random + +object RandomNumberUtil { + + private var randNumList:List = mutableListOf() + private var _num_count_min = 0; + private var _num_counter = 0; + private var _num_count_max = 5; + + fun num_increase():Boolean { + _num_counter++; + if (_num_counter == _num_count_max) return false + else return true; + } + + fun setRandDescending(min: Int, max: Int, count: Int){ + _num_count_min = min; + _num_count_max = count; + _num_counter = min; + + require(max >= min) { "max should be greater than or equal to min" } + require(count > 0) { "count should be greater than 0" } + + // 랜덤 숫자 생성 + val randomNumbers = List(count - 1) { Random.nextInt(min, max + 1) } // count-1개의 랜덤 값 생성 + + // 마지막 값을 0으로 추가 + val finalList = randomNumbers + 0 + + randNumList = finalList.sortedDescending() + // 내림차순 정렬 + } + + fun getCurrentNumber():Int { + return randNumList[_num_counter] + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/SpreadsheetAdapter.kt b/app/src/main/java/com/example/smart119/SpreadsheetAdapter.kt new file mode 100644 index 0000000..13ba646 --- /dev/null +++ b/app/src/main/java/com/example/smart119/SpreadsheetAdapter.kt @@ -0,0 +1,221 @@ +package com.example.smart119 + +import android.graphics.Color +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.LinearLayout +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.google.gson.JsonObject + +class SpreadsheetAdapter( + private var items: List, + private val onButtonClick: (ListItem.Row, Int) -> Unit, // button click 이벤트 + private val onRowClick: (ListItem.Row) -> Unit// Row click 이벤트 +) : RecyclerView.Adapter() { + + companion object { + const val VIEW_TYPE_HEADER = 0 + const val VIEW_TYPE_ROW = 1 + } + + override fun getItemViewType(position: Int): Int { + return when (items[position]) { + is ListItem.Header -> VIEW_TYPE_HEADER + is ListItem.Row -> VIEW_TYPE_ROW + else -> throw IllegalArgumentException("Unknown view type at position $position") + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + return if (viewType == VIEW_TYPE_HEADER) { + val view = inflater.inflate(R.layout.header_row, parent, false) + HeaderViewHolder(view) + } else { + val view = inflater.inflate(R.layout.data_row, parent, false) + RowViewHolder(view, onButtonClick, onRowClick) + } + } + + private var _columnCount = 0 + private var selectedPosition: Int = RecyclerView.NO_POSITION + private var headerList: List? = null + private var _IndexMap:MutableMap = mutableMapOf() + + fun setColumnCount(count:Int) { + _columnCount = count + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (val item = items[position]) { + is ListItem.Header -> (holder as HeaderViewHolder).bind(item) + is ListItem.Row -> (holder as RowViewHolder).bind(item, position) + else -> throw IllegalArgumentException("Unsupported item type at position $position") + } + + /* + holder.itemView.setOnClickListener { + val previousPosition = selectedPosition + selectedPosition = position + + // 이전 선택 항목과 현재 선택 항목 갱신 + notifyItemChanged(previousPosition) + notifyItemChanged(selectedPosition) + } + */ + } + + override fun getItemCount(): Int = items.size + + fun setSelectedHospitalPos(selPos:Int) { + selectedPosition = selPos + } + + fun getSelectedHospital() : ListItem? { + //return this.items[selectedPosition] + return if(selectedPosition in 0 until items.size) { + this.items[selectedPosition] + } else { + null + } + } + fun getSelectedPosition():Int { + var valid = selectedPosition - _columnCount; + if(valid < 0) valid = 0 + return valid + } + + fun getSelectedHospital_JSON() : JsonObject? { + //return this.items[selectedPosition] + var returnValue = JsonObject() + if(selectedPosition in 0 until items.size) { + var _row = (this.items[selectedPosition] as ListItem.Row); + returnValue = JsonObject().apply { + addProperty("name", "${_row.values[0]}") + addProperty("address", "${_row.values[1]}") + addProperty("tell","${_row.values[2]}") + addProperty("lidar","${_row.values[3]}") + addProperty("distance","${_row.values[4]}") + } + } + return returnValue + } + + private fun default_list_headers() { + this.items = listOf( + ListItem.Header(listOf("병원이름")), // 헤더 열 정의 + ListItem.Header(listOf("주소")), + ListItem.Header(listOf("전화번호")), + ListItem.Header(listOf("라이다 혼잡도")), + ListItem.Header(listOf("거리")), + ListItem.Header(listOf("병원별 이송 시작")), + ) + } + + fun hospitalListUpdate(searchResult:List>, idList: List) { + //기본 헤더 설정 + default_list_headers() + + //추가 검색된 결과 설정 +// var loop_count = 0; +// for (addItem in searchResult) { +// this.items = this.items + ListItem.Row(addItem) +// println(addItem[0]) +// loop_count++; +// } + + searchResult.forEachIndexed { index, addItem -> + this.items = this.items + ListItem.Row(addItem) + if (idList.size > index) { + _IndexMap[idList[index]] = this.items.size - 1 // ID와 리스트 인덱스 매핑 + } + } + + notifyDataSetChanged() + } + + fun getHospitalById(id: String): ListItem.Row? { + val position = _IndexMap[id] ?: return null + return if (position in 0 until items.size) { + items[position] as? ListItem.Row + } else { + null + } + } + + fun clearHospitalList() { + if(this.items.size == 6) return; + + if (selectedPosition != RecyclerView.NO_POSITION) selectedPosition = RecyclerView.NO_POSITION + + if (_IndexMap != null) _IndexMap.clear() + + default_list_headers() + notifyDataSetChanged() + } + +// fun getSelectedHospitalName(): String { +// return selRowHospName +// } + + // 헤더 ViewHolder + class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val columns: List = listOf( + itemView.findViewById(R.id.column1) // 추가 열에 따라 확장 가능 + ) + + fun bind(header: ListItem.Header) { + header.columns.forEachIndexed { index, column -> + columns[index].text = column + } + } + } + + // 데이터 행 ViewHolder + class RowViewHolder( + itemView: View, + private val onButtonClick: (ListItem.Row, Int) -> Unit, + private val onRowClick: (ListItem.Row) -> Unit + ) : RecyclerView.ViewHolder(itemView) { + private val cells: List = listOf( + itemView.findViewById(R.id.cell1), + itemView.findViewById(R.id.cell2), + itemView.findViewById(R.id.cell3), // 추가 열에 따라 확장 가능 + itemView.findViewById(R.id.cell4), + itemView.findViewById(R.id.cell5) + ) + + private val lContainer: LinearLayout = itemView.findViewById(R.id.cell6_container) + private val lContButton: Button = itemView.findViewById(R.id.cell6_button) + private val lLidarView : View = itemView.findViewById(R.id.cell4_lidar_color) + + fun bind(row: ListItem.Row, pos:Int) { + row.values.take(cells.size).forEachIndexed { index, value -> + cells[index].text = value + + if (index == 3) lLidarView.setBackgroundColor( + Color.parseColor(API_Helper.lidar_levelState_Color(value)) + ) + } + + /* + // 선택된 항목 하이라이트 적용 + if (isSelected) { + itemView.setBackgroundColor(Color.parseColor("#DCDCDC")) // 선택된 배경색 + onRowClick(row) + } else { + itemView.setBackgroundColor(Color.WHITE) // 기본 배경색 + } + */ + + lContButton.setOnClickListener { + onButtonClick(row, pos) // 버튼 클릭 시 Row의 position 전달 + } + + } + } +} diff --git a/app/src/main/java/com/example/smart119/TransferManager.kt b/app/src/main/java/com/example/smart119/TransferManager.kt new file mode 100644 index 0000000..6284563 --- /dev/null +++ b/app/src/main/java/com/example/smart119/TransferManager.kt @@ -0,0 +1,742 @@ +package com.example.smart119 + +import android.graphics.Color +import android.view.View +import android.widget.TextView +import com.example.smart119.datas.HospitalSearch +import com.example.smart119.datas.SaveLogin +import com.example.smart119.interfaces.RetrofitClient +import com.example.smart119.views.FragmentHandler +import com.google.android.material.bottomnavigation.BottomNavigationView +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.text.SimpleDateFormat +import java.util.Date +import java.util.concurrent.atomic.AtomicBoolean + +enum class TransferState { + IDLE, // 유휴상태 + STARTED, // 이송시작 + CANCELED, // 이송취소 + COMPLETED, // 이송완료 + END, // 이송 종료 + CHANGED // 이송변경 +} + +enum class ETAState { + CONTINUE, // API 계속 진행 상태 + END // API 정지 or Cancel +} + +object TransferManager { + @Volatile + private var currentState: TransferState = TransferState.IDLE + public var serverState = AtomicBoolean(false) + + @Volatile + private var ETA_STATE: ETAState = ETAState.CONTINUE + + private var transferThread: Thread? = null + private val isRunning = AtomicBoolean(false) + private val sleepTime: Long = 5000 + + private var isKeepFindPath = AtomicBoolean(false) + + private var isUpdateETA = AtomicBoolean(false) + private val updateDelayTime: Long = 10000; //5초 update eta 지연 수행 시간 + private val runningEvtListener = mutableMapOf Unit>() + + private var errorDefLon: Double = 127.00663305137988; + private var errorDefLat: Double = 37.56777199202061; + + private var _selectedHospInfo:JsonObject = JsonObject(); + + private var lastUpdateData:JsonObject = JsonObject().apply { + addProperty("hpid", "") + addProperty("levelState", "") + addProperty("eta", "") + addProperty("currentTime", "") + addProperty("leftDistance", "") + addProperty("transferState", "") + }; + + fun set_transfer_data( + hpid:String, levelState:String, eta:String, + currentTime:String, leftDistance:String, transferState:String + ) { + if (hpid != "") lastUpdateData.addProperty("hpid", hpid) + if (levelState != "") lastUpdateData.addProperty("levelState", levelState) + if (eta != "") lastUpdateData.addProperty("eta", eta) + if (currentTime != "") lastUpdateData.addProperty("currentTime", currentTime) + if (leftDistance != "") lastUpdateData.addProperty("leftDistance", leftDistance) + if (transferState != "") { + + lastUpdateData.addProperty("transferState", transferState) + } + } + fun get_transfer_data():JsonObject { + return lastUpdateData + } + // 이벤트 리스너 등록 + fun registerListener(key: String, listener: (JsonObject?) -> Unit) { + if (runningEvtListener.containsKey(key)) { +// println("Listener with key '$key' already registered.") + } else { + runningEvtListener[key] = listener +// println("Listener with key '$key' registered.") + } + } + + // 이벤트 발생 + private fun dispatchEvent(eventName: String, data: JsonObject? = null) { + val listener = runningEvtListener[eventName] + if (listener != null) { + println("Dispatching event: $eventName") + listener(data) + } else { + println("No listener registered for event: $eventName") + } + } + + fun getState(): TransferState { + return currentState + } + + + // 이송 시작 + fun startTransfer():Boolean { + if (currentState == TransferState.STARTED) { + println("Transfer is already started.") + return false + } + + currentState = TransferState.STARTED + ETA_STATE = ETAState.CONTINUE; + isRunning.set(true) + isKeepFindPath.set(true) + // "/startEmergency.do" 먼저 실행 후, 결과에 따라 thread 시작 + + try { + _SET_HOSPINFO(HospitalSearch.getSelectedData()) + UpdateTransferInfomation(); + } + catch (e:Exception) { + println(e.message) + } + + if (API_STARTEMG()) { + transferThread = Thread { + println("Transfer started.") + while (isRunning.get() && currentState == TransferState.STARTED) { + try { + // 이송 중 반복 작업 수행 + println("Transfer in progress... SLEEP TIME[${sleepTime}ms]") + // 등록한 이벤트 함수로 update중인 상태 전달 +// if (!RandomNumberUtil.num_increase()) { +// println("END!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") +// API_END_TRANSFER(); +// continue +// } + //#region 서버로부터 종료 코드를 받는 경우, + //#endregion + +// println("이전 데이터"); +// println(HospitalSearch.getSelectedData()); +// println("변경 데이터"); +// println(_GET_HOSPINFO()); + + Thread.sleep(sleepTime) // 1초마다 실행 + } catch (e: InterruptedException) { + println("Transfer loop interrupted.") + } + } + println("Transfer loop stopped.") + isKeepFindPath.set(false); + } + + transferThread?.start() + return true; + } else { + isKeepFindPath.set(false); + SendDialogMsg("응급 이송 시작 오류", "API START시 오류 발생") + return false; + } + } + + private fun _SET_HOSPINFO(hospInfo:JsonObject?) { + val copiedJson = JsonParser.parseString(hospInfo.toString()).asJsonObject + _selectedHospInfo = copiedJson + } + + private fun _GET_HOSPINFO():JsonObject? { + return _selectedHospInfo; + } + + fun SendDialogMsg(title:String, msg:String) { + var sendMsg = JsonObject().apply { + addProperty("title", title) + addProperty("msg", msg) + } + +// val transferHandler = FragmentHandler.getView("TransferFragment"); +// if (transferHandler != null) { +// val bottomNaviView = transferHandler.findViewById(R.id.bottom_navigation) +// bottomNaviView.selectedItemId = R.id.hospSearch; +// } + dispatchEvent("transfer_dialogmsg_event", sendMsg) + } + + // 이송 취소 + fun cancelTransfer() { + if (currentState != TransferState.STARTED && + serverState == AtomicBoolean(false)) { + println("Cannot cancel. Transfer is not active.") + return + } + + currentState = TransferState.CANCELED + println("Transfer set CANCELED") + API_END_TRANSFER() + } + + // 이송 종료 + fun completeTransfer() { + if (currentState != TransferState.STARTED) { + println("Cannot complete. Transfer is not active.") + return + } + + currentState = TransferState.COMPLETED + println("Transfer set COMPLETED") + API_END_TRANSFER() + stopTransferLoop() + } + + // 이송 변경 + fun changeTransfer() { + if (currentState != TransferState.STARTED) { + println("Cannot change. Transfer is not active.") + return + } + +// currentState = TransferState.CHANGED + ///updateETDHospId.do + println("Transfer state changed.") + // 이송 변경에 따른 추가 작업 수행 + API_CHANGE_HOSPITAL() + } + + // 이송 종료 상태로 설정 + fun setEnd() { + if (currentState == TransferState.STARTED) { + isRunning.set(false) + isKeepFindPath.set(false) + transferThread?.interrupt() + transferThread = null + } + currentState = TransferState.END; + println("Transfer set to END state.") + + SendDialogMsg("이송 종료", "응급 이송이 종료되었습니다.") + } + + fun setCancel() { + isRunning.set(false) + isKeepFindPath.set(false) + transferThread?.interrupt() + transferThread = null + + currentState = TransferState.END; + SendDialogMsg("이송 종료", "응급 이송이 취소되었습니다.") + } + + // IDLE 상태로 설정 + fun setIdle() { + if (currentState == TransferState.STARTED) { + stopTransferLoop() + } + currentState = TransferState.IDLE + println("Transfer set to IDLE state.") + } + + // 루프 중단 + private fun stopTransferLoop() { + isRunning.set(false) + isKeepFindPath.set(false) + transferThread?.interrupt() + transferThread = null + ETA_STATE = ETAState.END + + currentState = TransferState.IDLE + SendDialogMsg("응급 이송 종료", "이송이 종료되었습니다.") + } + + // 현재 상태 조회 + fun getCurrentState(): TransferState { + return currentState + } + + //pingpong을 위한 메시지 api 수행 + private fun FindPath_ETA() { + isUpdateETA.set(false) + if (!isRunning.get() && currentState != TransferState.STARTED) return; + + try { +// val resObj = res.getAsJsonObject(); +// val callResult = resObj["isSuccess"]?.asBoolean ?: true; + val lon = LocationHelper.getCurrentLon(); + val lat = LocationHelper.getCurrentLat(); +// val lon = resObj["lon"]?.asDouble ?: 0.0; +// val lat = resObj["lat"]?.asDouble ?: 0.0; + + val src_pos = "${lon}, ${lat}"; + + val selectedHospital = _GET_HOSPINFO(); + var hosp_lon = selectedHospital?.get("lon")?.asString + var hosp_lat = selectedHospital?.get("lat")?.asString + if (hosp_lon == "null" || hosp_lon == "") { + hosp_lon = errorDefLon.toString() + } + if (hosp_lat == "null" || hosp_lat == "") { + hosp_lat = errorDefLat.toString() + } + var dst_pos = "${hosp_lon}, ${hosp_lat}"; + + if (isKeepFindPath.get()) { + HelpUtility.delay_executor(updateDelayTime) { + RetrofitClient.pathInstance.findPath( + API_Helper.make_findpath_param(src_pos, dst_pos) + ).enqueue(findPathResponse()) + } + } + else { + UpdateTransferInfomation_TIME() + } + + + } catch (e: Exception) { + println(e.message) +// SendDialogMsg("오류 발생", "길찾기 중 오류가 발생하였습니다. ${e.message}") + } + + } + + private fun UpdateTransferInfomation_ETA(loc:JsonObject) { + + try { + var tfView = FragmentHandler.getView("TransferFragment") + if (tfView != null) { + var _eta = loc["eta"].asInt; + var _leftDistance = loc["leftDistance"].asString + + val currentTime = SimpleDateFormat("HH:mm:ss").format(Date()) + val updateTimeTxt = tfView.findViewById(R.id.infoUpdateTime) + updateTimeTxt?.text = currentTime; + + var _hospInfo = _GET_HOSPINFO() + //#region eta 업데이트 + if (_eta != -1 && _eta != null) { + val etaTxt = tfView.findViewById(R.id.tvLeftTimeVal) + val _etaStrSec = API_Helper.ETA_SEC_TO_STRING(_eta); + etaTxt?.text = _etaStrSec + if (_hospInfo != null) { _hospInfo.addProperty("eta", _etaStrSec) } + } + //#endregion + + //#region etd 업데이트 + if (_leftDistance != "" && _leftDistance != null) { + val etdTxt = tfView.findViewById(R.id.tvLeftDistVal) + val _etdStrKM = API_Helper.DISTANCE_FORMAT_METER_TO_KM(_leftDistance); + etdTxt?.text = _etdStrKM; + if (_hospInfo != null) { _hospInfo.addProperty("leftDistance", _etdStrKM) } + } + //#endregion + } + } catch (e: Exception) { + println(e.message) + } + } + + private fun UpdateTransferInfomation_TIME() { + var tfView = FragmentHandler.getView("TransferFragment") + if (tfView != null) { + val currentTime = SimpleDateFormat("HH:mm:ss").format(Date()) + val updateTimeTxt = tfView.findViewById(R.id.infoUpdateTime) + updateTimeTxt?.text = currentTime + } + } + + private fun UpdateTransferInfomation() { + try { + var tfView = FragmentHandler.getView("TransferFragment") + if (tfView != null) { + //병원 이름, 혼잡도 + var hosp_info = _GET_HOSPINFO(); + if (hosp_info != null) { + println(hosp_info) + var levelState = hosp_info["levelState"].asString + var hospName = hosp_info["title"].asString + + val hospNameTxt = tfView.findViewById(R.id.tvTFHospName) + hospNameTxt?.text = hospName + val lidarValTxt = tfView.findViewById(R.id.tvTFLidarValue) + lidarValTxt?.text = levelState + + val lLidarView : View = tfView.findViewById(R.id.tvTFLidarColor) + lLidarView?.setBackgroundColor( + Color.parseColor(API_Helper.lidar_levelState_Color(levelState)) + ) + + val tfStateTxt = tfView.findViewById(R.id.tfState) + when (currentState) { + TransferState.IDLE -> tfStateTxt.text = "유휴 상태" + TransferState.STARTED -> tfStateTxt.text = "이송 시작" + TransferState.CANCELED -> tfStateTxt.text = "이송 취소" + TransferState.COMPLETED -> tfStateTxt.text = "이송 완료" + TransferState.END -> tfStateTxt.text = "이송 종료" + else -> tfStateTxt.text = "-" + } + + println() + } + } + } catch (e: Exception) { + println(e.message) + } + } + fun SetLastHospitalInfo() { + try { + var tfView = FragmentHandler.getView("TransferFragment") + if (tfView != null) { + //병원 이름, 혼잡도 + var hosp_info = _GET_HOSPINFO(); + if (hosp_info != null) { + println(hosp_info) + var levelState = hosp_info["levelState"].asString + var hospName = hosp_info["title"].asString + + val hospNameTxt = tfView.findViewById(R.id.tvTFHospName) + hospNameTxt?.text = hospName + val lidarValTxt = tfView.findViewById(R.id.tvTFLidarValue) + lidarValTxt?.text = levelState + val lLidarView : View = tfView.findViewById(R.id.tvTFLidarColor) + lLidarView?.setBackgroundColor( + Color.parseColor(API_Helper.lidar_levelState_Color(levelState)) + ) + + val tfStateTxt = tfView.findViewById(R.id.tfState) + when (currentState) { + TransferState.IDLE -> tfStateTxt.text = "유휴 상태" + TransferState.STARTED -> tfStateTxt.text = "이송 시작" + TransferState.CANCELED -> tfStateTxt.text = "이송 취소" + TransferState.COMPLETED -> tfStateTxt.text = "이송 완료" + TransferState.END -> tfStateTxt.text = "이송 종료" + else -> tfStateTxt.text = "-" + } + + var last_eta = "-"; + var last_etd = "-"; + //#region last eta 업데이트 + val etaTxt = tfView.findViewById(R.id.tvLeftTimeVal) + if (hosp_info["eta"] != null) { + last_eta = hosp_info["eta"].asString + etaTxt?.text = last_eta + } + //#endregion + + //#region last etd 업데이트 + val etdTxt = tfView.findViewById(R.id.tvLeftDistVal) + if (hosp_info["leftDistance"] != null) { + last_etd = hosp_info["leftDistance"].asString + etdTxt?.text = last_etd; + } + //#endregion + + UpdateTransferInfomation_TIME(); + } + } + } catch (e: Exception) { + println(e.message) + } + } + + private fun API_STARTEMG(): Boolean { + val selectedData = HospitalSearch.getSelectedData() + if (selectedData != null) { + var hpid = selectedData["hpid"].toString().trim('"') + var reqBody = JsonObject().apply { + addProperty("hpid", hpid) + addProperty("hosp_name", selectedData.asJsonObject["title"].asString) + } + RetrofitClient.transferInstance.startEMG(reqBody).enqueue(startTransferResponse()) + return true; + } else return false; + } + + // 업데이트 ETA API 함수 + private fun API_UPDATEETA(reqBody:JsonObject): Boolean { + // API URL : /updateETDDuration + RetrofitClient.transferInstance.updateHospETA(reqBody).enqueue(updateETAResponse()) + return false; + } + + private fun API_CHANGE_HOSPITAL(): Boolean { + val selectedData = HospitalSearch.getSelectedData() + if (selectedData != null) { + var _sel_hpid = selectedData["hpid"].asString + var _updateETDhospId_body = JsonObject().apply { + addProperty("hpid", _sel_hpid) + addProperty("hosp_name", selectedData.asJsonObject["title"].asString) + } + RetrofitClient.transferInstance.changeHospital(_updateETDhospId_body).enqueue(changeHospitalResponse()) + return true; + } + return false; + } + + private fun API_END_TRANSFER(): Boolean { + RetrofitClient.transferInstance.endEMG().enqueue(endTransferResponse()) + return true; + } + + private fun startTransferResponse(): Callback { + return object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { //200 ok + val resBody = response.body(); + if (resBody != null) { + var _status = resBody.get("status")?.asString; + //response.status = "success" or "duplicate" + if (_status == "success") { + UpdateTransferInfomation() + FindPath_ETA(); + println("이송 시작 [onResponse - Success] : ${response.message()}") + } + else { + println("이송 시작 [onResponse - Fail] : ${response.message()}") + //검색창, 다시 환자 정보 전송 + SendDialogMsg("동일한 환자정보가 이미 존재합니다", + "같은 차량으로 동일한 환자정보가 이미 전송되어 있습니다.") +// nav_to_hospsearch(); + } + } + else { + + } + } else { //200 아닌 코드들 + println("이송 시작 [onResponse - Fail] : ${response.message()}") + val parseBody = JsonParser.parseString(response.errorBody()?.string()) + if (parseBody != null) { + val resStatus = parseBody.asJsonObject["status"].asString + if (resStatus == "duplicate") { + SendDialogMsg("환자정보 전송 실패", + "같은 차량으로 동일한 환자정보가 이미 전송되어 있습니다.") + clear_state(); + } + else { + SendDialogMsg("환자정보 전송 실패", + "알수없는 오류로 전송에 실패하였습니다. 담당자에게 문의해주세요.") + clear_state(); + } + } + else { + SendDialogMsg("환자정보 전송 실패", + "알수없는 오류로 전송에 실패하였습니다. 담당자에게 문의해주세요.") + clear_state(); + } + } + } + + override fun onFailure(call: Call, t: Throwable) { + println("이송 시작 [onFailure] ${t.message}") + SendDialogMsg("환자 정보 전송 실패", + "알수없는 오류로 전송에 실패하였습니다. 담당자에게 문의해주세요.") + clear_state(); + } + } + } + + // 업데이트 ETA API 결과 + private fun updateETAResponse(): Callback { + return object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + var resBody = response.body() + var _status = resBody?.get("status")?.asString; + if (_status == "continue") { + FindPath_ETA(); + println("API_ETA_TRANSFER[onResponse - Success] : ${response.message()}") + } + else { + //이송 종료 처리 + if (currentState != TransferState.END) setEnd() + } + } else { + println("API_ETA_TRANSFER[onResponse - Fail] : ${response.message()}") + isKeepFindPath.set(false) + } + } + + override fun onFailure(call: Call, t: Throwable) { + //대부분 네트워크 문제 + println("API_ETA_TRANSFER[OnFailure] : ${t.message}") + isKeepFindPath.set(false) + } + } + } + + // 업데이트 ETA API 결과 + private fun changeHospitalResponse(): Callback { + return object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + try { + _SET_HOSPINFO(HospitalSearch.getSelectedData()) + UpdateTransferInfomation(); + } + catch (e:Exception) { + println(e.message) + } + println("API_CHANGE_HOSPITAL[onResponse - Success] : ${response.message()}") + } else { + println("API_CHANGE_HOSPITAL[onResponse - Fail] : ${response.message()}") + isKeepFindPath.set(false) + SendDialogMsg("병원 정보 변경 실패", + "알수없는 오류로 전송에 실패하였습니다. 담당자에게 문의해주세요.") + clear_state(); + } + } + + override fun onFailure(call: Call, t: Throwable) { +// ETA_STATE = ETAState.END //? + println("API_CHANGE_HOSPITAL[onFailure] : ${t.message}") + + } + } + } + + // 업데이트 ETA API 결과 + private fun endTransferResponse(): Callback { + return object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + var resBody = response.body() + var _status = resBody?.get("status")?.asString; + setCancel() + } else { +// ETA_STATE = ETAState.END //? + println("API_END_TRANSFER[onResponse - Fail] : ${response.message()}") + isKeepFindPath.set(false) + clear_state() + } + } + + override fun onFailure(call: Call, t: Throwable) { +// ETA_STATE = ETAState.END //? + //대부분 네트워크 문제 +// println("API_END_TRANSFER[OnFailure] : ${t.message}") + isKeepFindPath.set(false) + clear_state() + } + } + } + + //길찾기 API 결과 + private fun findPathResponse(): Callback { + return object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + try { + var resBody = response.body() + var routes = resBody?.get("routes")?.asJsonArray + if (routes != null && routes.size() > 0) { + var f_route = routes[0].asJsonObject; + var res_code = f_route["result_code"].asString + when(res_code) { + "0" -> { + var f_summary = f_route["summary"].asJsonObject + var _distance = f_summary["distance"].asString; + var _duration = f_summary["duration"].asString; + var _updateETA = JsonObject().apply { + addProperty("leftDistance", _distance) + addProperty("eta", _duration) + } + + //duration : 초, distance : 미터 + println("distance : ${_distance}km, duration : ${_duration}sec") + UpdateTransferInfomation_ETA(_updateETA) + + var _updateETA_body = JsonObject().apply { + addProperty("duration", _duration) + } + API_UPDATEETA(_updateETA_body) + } + "1" -> FindPath_ETA() // 길찾기 결과를 찾을 수 없음, 종료 + "101" -> FindPath_ETA() // 경유지 지점 주변의 도로를 탐색할 수 없음, 계속 찾기 + "102" -> FindPath_ETA() // 시작 지점 주변의 도로를 탐색할 수 없음, 계속 찾기 + "103" -> FindPath_ETA() // 도착 지점 주변의 도로를 탐색할 수 없음, 계속 찾기 + "104" -> isKeepFindPath.set(false) // 출발지와 도착지가 5 m 이내로 설정된 경우 경로를 탐색할 수 없음, 이 경우엔 패스 + "105" -> FindPath_ETA() // 시작 지점 주변의 도로에 유고 정보(교통 장애)가 있음, 계속 찾기 + "106" -> FindPath_ETA() // 도착 지점 주변의 도로에 유고 정보(교통 장애)가 있음, 계속 찾기 + "107" -> FindPath_ETA() // 경유지 주변의 도로에 유고 정보(교통 장애)가 있음, 계속 찾기 + else -> FindPath_ETA() // 길찾기 시 알수 없는 코드가 나왔을때, + } + } + } catch (e: Exception) { + println(e.message) + isKeepFindPath.set(false) + } + } else { + println("길찾기 결과[Fail] : ${response.message()}") + isKeepFindPath.set(false) + } + } + + override fun onFailure(call: Call, t: Throwable) { + //대부분 네트워크 문제 + println("길찾기 결과[OnFailure] : ${t.message}") + isKeepFindPath.set(false) + } + } + } + + fun nav_to_login() { + val activity = AlertDialogUtil.getActivityContext() + if (activity != null) { + AlertDialogUtil.showAlert_OK( + context = activity, + title = "세션 만료", + message = "기존 세션이 만료되었습니다. 다시 로그인 해주시기 바랍니다.\n", + onPositiveClick = { + SaveLogin.setLogin(false) + LoginManager.setLoggedIn(false); + val bottomNaviView = + activity.findViewById(R.id.bottom_navigation) + bottomNaviView.selectedItemId = R.id.emgLogin; + } + ) + } + } + + fun nav_to_hospsearch() { + val activity = AlertDialogUtil.getActivityContext() + val bottomNav = activity?.findViewById(R.id.bottom_navigation) + if (bottomNav != null && + bottomNav.selectedItemId != R.id.hospSearch + ){ + bottomNav.selectedItemId = R.id.hospSearch + } + } + + fun clear_state() { + currentState = TransferState.IDLE + isRunning.set(false) + isKeepFindPath.set(false) + transferThread?.interrupt() + transferThread = null + println("Logout, Clear Transfer State.") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/ViewPagerAdapter.kt b/app/src/main/java/com/example/smart119/ViewPagerAdapter.kt new file mode 100644 index 0000000..74864be --- /dev/null +++ b/app/src/main/java/com/example/smart119/ViewPagerAdapter.kt @@ -0,0 +1,20 @@ +package com.example.smart119 + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.example.smart119.views.AddressSearchFragment +import com.example.smart119.views.LocationSearchFragment + +class ViewPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) { + override fun getItemCount(): Int = 2 // 탭 개수 + + override fun createFragment(position: Int): Fragment { + return when (position) { + 0 -> AddressSearchFragment() + 1 -> LocationSearchFragment() + else -> AddressSearchFragment() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/datas/HospitalSearch.kt b/app/src/main/java/com/example/smart119/datas/HospitalSearch.kt new file mode 100644 index 0000000..a3c0736 --- /dev/null +++ b/app/src/main/java/com/example/smart119/datas/HospitalSearch.kt @@ -0,0 +1,484 @@ +package com.example.smart119.datas + +import android.content.Context +import android.view.LayoutInflater +import android.widget.PopupWindow +import android.widget.Toast +import com.example.smart119.R +import com.example.smart119.interfaces.RetrofitClient +import com.example.smart119.views.FragmentHandler +import com.example.smart119.views.HospitalInfoPopup +import com.example.smart119.views.HospitalSearchPopup +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +object HospitalSearch { + private val savedHospList: MutableList = mutableListOf() + private var selectedHospitalIdx: Int = -1 + private var bUsedFilter: Boolean = false + private var gpsAddress:String = ""; + private var hospDetail = mutableMapOf( + "isTransfer" to false, + "info" to mutableMapOf() + ) + private val DEF_SEARCH:JsonObject = JsonObject().apply { + addProperty("sidoName", "서울특별시") + addProperty("sidoCode", "11") + } + private var prevLon:Double = 0.0; + private var prevLat:Double = 0.0; + private var prevTime:Long = 0; + + private var requestBody:JsonObject = JsonObject().apply { + addProperty("bFilter", false) + add("reqBody", JsonObject()) + } + + private lateinit var popupSearch: PopupWindow; + + private var sidoInfo:JsonObject = JsonObject().apply { + addProperty("isLoaded", false) + } + private var gunGuInfo:JsonObject = JsonObject().apply { + addProperty("isLoaded", false) + } + private var eubMyeonDongInfo:JsonObject = JsonObject().apply { + addProperty("isLoaded", false) + } + private var gunGuNameList:MutableList = mutableListOf() + + private var gunGuSelect:String = "군구선택"; + private var hospCatSelect:String = "병원분류명선택"; + private var prtSelect:String = "진료과목선택"; + + private var addrFilterSave:JsonObject = JsonObject().apply { + addProperty("si", "") + addProperty("gu", "") + addProperty("relate", "") + } + + //true면 주소검색 사용, false면 위경도 검색 사용 + fun SET_SearchRequest(isAddress:Boolean, reqBody:JsonObject) { + requestBody.addProperty("bFilter", isAddress) + requestBody.add("reqBody", reqBody) + } + fun GET_SearchRequest():JsonObject { + return requestBody; + } + + fun isAddressSearch():Boolean { + return requestBody.get("bFilter")?.asBoolean ?: false + } + + fun setPrevGPSUpdate(lon:Double, lat:Double, time:Long) { + prevLon = lon; + prevLat = lat; + prevTime = time; + println("updated!!! ${prevLon}, ${prevLat}, ${prevTime}") + } + fun getPrevGPSInfo():JsonObject { + return JsonObject().apply { + addProperty("prevLon", prevLon) + addProperty("prevLat", prevLat) + addProperty("prevTime", prevTime) + } + } + + fun itemStructure( + hpid: String = "", // 병원 아이디 + levelState: String = "", // lidar 레벨 + distance: String = "", // 거리 + city: String = "", // 시 + district: String = "", // 구 + tel: String = "", // 전화번호 + title: String = "", // 병원 이름 + lon: String = "", // 경도 + lat: String = "" // 위도 +// name: String = "", +// address: String = "", +// phoneNumber: String = "", +// lidarState: String = "" + ):JsonObject { + return JsonObject().apply({ + addProperty("hpid", hpid) + addProperty("levelState", levelState) + addProperty("distance", distance) + addProperty("city", city) + addProperty("district", district) + addProperty("tel", tel) + addProperty("title", title) + addProperty("lon", lon) + addProperty("lat", lat) + }) + } + + fun init_hospital_req(context:Context) { + var sidoData = sidoInfo.getAsJsonObject("data"); + //비어있는 경우에만 요청하여 얻기 + if (sidoData == null || sidoData.entrySet().isEmpty()) { + //1. 시 관련 콤보박스 먼저 얻기 + RetrofitClient.locInstance.getSidoList().enqueue(sidoResponse()) + } + } + + fun get_gps_address():String { + return gpsAddress + } + + fun set_gps_update(address:String) { + if(gpsAddress.isEmpty()) { + gpsAddress = address + } + } + + fun set_selectGunGu(gunGu:String = "군구선택") { + gunGuSelect = gunGu + } + fun get_selectGunGu():String { + return gunGuSelect + } + + fun set_selectHospCatAddress(hospCat:String = "병원분류명선택") { + hospCatSelect = hospCat + } + fun get_selectHospCatAddress():String { + return hospCatSelect + } + + fun set_selectPrtAddress(prt:String = "진료과목선택") { + prtSelect = prt + } + fun get_selectPrtAddress():String { + return prtSelect + } + + fun set_popup_search(searchPopup:PopupWindow) { + popupSearch = searchPopup + } + fun get_popup_search(): PopupWindow { + return popupSearch + } + + fun req_gun_list(si:String) { + RetrofitClient.locInstance.getSiGunGuList(JsonObject().apply { + addProperty("code", get_si_code(si) ?: "") + }).enqueue(siGunGuResponse()) + } + + private fun get_si_code(si:String): String { + return sidoInfo["data"].asJsonObject["${si}"].asString + } + + private fun _itemStructure_IDX( + idx: Int = 0, + hpid: String = "", // 병원 아이디 + levelState: String = "", // lidar 레벨 + distance: String = "", // 거리 + city: String = "", // 시 + district: String = "", // 구 + tel: String = "", // 전화번호 + title: String = "", // 병원 이름 + lon: String = "", // 경도 + lat: String = "" // 위도 + ):JsonObject { + return JsonObject().apply({ + addProperty("idx", idx) + addProperty("hpid", hpid) + addProperty("levelState", levelState) + addProperty("distance", distance) + addProperty("city", city) + addProperty("district", district) + addProperty("tel", tel) + addProperty("title", title) + addProperty("lon", lon) + addProperty("lat", lat) + }) + } + // JsonObject 추가 + fun addData(jsonObject: JsonObject) { + savedHospList.add(jsonObject) + } + + fun setSearchFilter(si:String, gu:String, relate:String) { + addrFilterSave.addProperty("si", when(si) {"시도선택" -> "" else -> si }) + addrFilterSave.addProperty("gu", when(gu){"구군선택" -> "" else -> gu }) + addrFilterSave.addProperty("relate", relate) + } + + fun setFiltered(bFiltered:Boolean) { + bUsedFilter = bFiltered; + } + + fun getSearchInfo():JsonObject { + return addrFilterSave; + } + + fun getIsFiltered():Boolean { + return bUsedFilter + } + + fun addDataHospitalItems(hospList:List) { + var idx = 0; + hospList.forEach{ + hosp -> + var rebuild = _itemStructure_IDX( + idx++, + hosp["hpid"].toString().trim('"'), + hosp["levelState"].toString().trim('"'), + hosp["distance"].toString().trim('"'), + hosp["city"].toString().trim('"'), + hosp["district"].toString().trim('"'), + hosp["tel"].toString().trim('"'), + hosp["title"].toString().trim('"'), + hosp["lon"].toString().trim('"'), + hosp["lat"].toString().trim('"') + ) + savedHospList.add(rebuild) + } +// println(savedHospList); + } + + // 인덱스로 JsonObject 가져오기 + fun getData(index: Int): JsonObject? { + return if (index in savedHospList.indices) savedHospList[index] else null + } + + fun setIdx(idx:Int) { + selectedHospitalIdx = idx + } + + fun getSelectedData(): JsonObject? { + if (selectedHospitalIdx != -1) { +// var returnData = JsonObject().apply { } + val returnData = savedHospList.getOrNull(selectedHospitalIdx) + return returnData + } + else return null + } + + //region hospital detail용 함수 + fun getHospitalID():String { + if (selectedHospitalIdx != -1) { + val returnData = savedHospList.getOrNull(selectedHospitalIdx) + return returnData?.get("hpid")?.asString ?: ""; + } + else return "" + } + fun setHospitalDetail(info:JsonObject) { + hospDetail["info"] = info; + } + fun getHospitalDetail():Any? { + return hospDetail["info"] + } + //endregion + + fun getAllData(withIndex:Boolean = false):List> { + val setDataList:MutableList> = mutableListOf() + savedHospList.forEach { + json -> + if(withIndex) { + val address = + json["city"].toString().replace("\"", "") + " " + + json["district"].toString().replace("\"", "") + setDataList.apply { + add( + listOf( + json["idx"].toString().replace("\"", ""), + json["title"].toString().replace("\"", ""), + address, + json["tel"].toString().replace("\"", ""), + json["levelState"].toString().replace("\"", ""), + json["distance"].toString().replace("\"", "") + "km" + ) + ) + } + } + else { + val address = + json["city"].toString().replace("\"", "") + " " + + json["district"].toString().replace("\"", "") + setDataList.apply { + add( + listOf( + json["title"].toString().replace("\"", ""), + address, + json["tel"].toString().replace("\"", ""), + json["levelState"].toString().replace("\"", ""), + json["distance"].toString().replace("\"", "") + "km" + ) + ) + } + } + } + return setDataList + } + + fun clearAllData() { + selectedHospitalIdx = 0; + savedHospList.clear() + } + + private fun sidoResponse(): Callback { + return object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + val jsonObject = response.body(); + var dataSet = JsonObject() +// println("sido response success ${jsonObject}"); + if (jsonObject != null) { + val resDatas = jsonObject["resData"].asJsonArray; + resDatas.forEach { data -> + val _sicode = data.asJsonObject["sidoCode"].asString + val _siname = data.asJsonObject["sidoName"].asString + dataSet.addProperty(_siname, _sicode) + } + } + //loaded true, data set + sidoInfo.addProperty("isLoaded", true) + sidoInfo.add("data", dataSet) + + //#region 현재 서울시를 중점으로 하기 때문에, 기본 코드 API요청 + req_gun_list(DEF_SEARCH["sidoName"].asString) + //#endregion + + } else { + println("sido response but fail"); + sidoDeafult() + } + } + + override fun onFailure(call: Call, t: Throwable) { + println("sido response fail ${t.message}"); + sidoDeafult() + } + } + } + private fun sidoDeafult() { + + var dataSet = JsonObject() + val default_si = DEF_SEARCH["sidoName"].asString; + val default_siCode = DEF_SEARCH["sidoCode"].asString; + dataSet.addProperty(default_si, default_siCode) + + sidoInfo.addProperty("isLoaded", false) + sidoInfo.add("data", dataSet) + + req_gun_list(default_si) + } + fun GET_SIDO_DEFAULT_NAME():String { + return DEF_SEARCH["sidoName"].asString + } + fun GET_SIDO_DEFAULT_CODE():String { + return DEF_SEARCH["sidoCode"].asString + } + + fun getSidoList():List { + var returnList: List = mutableListOf() + if (sidoInfo["isLoaded"].asBoolean) { + var sidoJsonObject = sidoInfo.getAsJsonObject("data") + returnList = sidoJsonObject.entrySet().map { it.key } + } + return returnList; + } + fun getGunGuNameList():MutableList { + return gunGuNameList.toMutableList(); + } + + private fun siGunGuResponse(): Callback { + return object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + val jsonObject = response.body(); + var dataSet = JsonObject(); +// println("sido response success ${jsonObject}"); + if (jsonObject != null) { + val resDatas = jsonObject["resData"].asJsonArray; + resDatas.forEach { data -> + val _guguncode = data.asJsonObject["code"].asString + val _gugunName = data.asJsonObject["name"].asString + dataSet.addProperty(_gugunName, _guguncode) + gunGuNameList.add(_gugunName) + } + } + //loaded true, data set + + gunGuInfo.addProperty("isLoaded", true) + gunGuInfo.add("data", dataSet) + + //세팅 구군 리스트 +// var hsfView = FragmentHandler.getView("HospitalSearchFragment") +// if (hsfView != null) { +// nameList.add(0, "구군선택") +// HospitalSearchPopup.set_POPUP_GUGUN_COMBO(popupSearch, hsfView.context, nameList) +// } + } else { + println("gugun response but fail"); + SEOUL_DEFAULT_GUNGU() + } + } + + override fun onFailure(call: Call, t: Throwable) { + println("gugun response fail ${t.message}"); + SEOUL_DEFAULT_GUNGU() + } + } + } + + + private fun siEubMyeonDongResponse(): Callback { + return object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + } else { + } + } + + override fun onFailure(call: Call, t: Throwable) { + } + } + } + + private fun SEOUL_DEFAULT_GUNGU() { + var dataSet = JsonObject(); + var nameList:MutableList = mutableListOf() + + dataSet.addProperty("11680","강남구") + dataSet.addProperty("11740","강동구") + dataSet.addProperty("11305","강북구") + dataSet.addProperty("11500","강서구") + dataSet.addProperty("11620","관악구") + dataSet.addProperty("11215","광진구") + dataSet.addProperty("11530","구로구") + dataSet.addProperty("11545","금천구") + dataSet.addProperty("11350","노원구") + dataSet.addProperty("11320","도봉구") + dataSet.addProperty("11230","동대문구") + dataSet.addProperty("11590","동작구") + dataSet.addProperty("11440","마포구") + dataSet.addProperty("11410","서대문구") + dataSet.addProperty("11650","서초구") + dataSet.addProperty("11200","성동구") + dataSet.addProperty("11290","성북구") + dataSet.addProperty("11710","송파구") + dataSet.addProperty("11470","양천구") + dataSet.addProperty("11560","영등포구") + dataSet.addProperty("11170","용산구") + dataSet.addProperty("11380","은평구") + dataSet.addProperty("11110","종로구") + dataSet.addProperty("11140","중구") + dataSet.addProperty("11260","중랑구") + + for((key, value) in dataSet.entrySet()) { + gunGuNameList.add(value.asString) + } + + gunGuInfo.addProperty("isLoaded", false) + gunGuInfo.add("data", dataSet) + + println(); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/datas/SampleLocation.kt b/app/src/main/java/com/example/smart119/datas/SampleLocation.kt new file mode 100644 index 0000000..ef5c0ce --- /dev/null +++ b/app/src/main/java/com/example/smart119/datas/SampleLocation.kt @@ -0,0 +1,4 @@ +package com.example.smart119.datas + +object SampleLocation { +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/datas/SaveLogin.kt b/app/src/main/java/com/example/smart119/datas/SaveLogin.kt new file mode 100644 index 0000000..9e3f0af --- /dev/null +++ b/app/src/main/java/com/example/smart119/datas/SaveLogin.kt @@ -0,0 +1,38 @@ +package com.example.smart119.datas + +import android.content.Context +import android.content.SharedPreferences + +object SaveLogin { + private const val PREF_119_NAME = "SMART119_LOGIN" + private const val KEY_IS_LOGGED_IN = "isLoggedIn" + private const val KEY_STR_USER_ID = "strUserId" + private const val KEY_STR_CAR_NO = "strCarNo" + + private lateinit var preferences: SharedPreferences + + fun init(context: Context) { + preferences = context.getSharedPreferences(PREF_119_NAME, Context.MODE_PRIVATE) + } + + fun setLogin(isLoggedIn:Boolean) { + preferences.edit().putBoolean(KEY_IS_LOGGED_IN, isLoggedIn).apply() + } + fun setUserId(strUserId:String) { + preferences.edit().putString(KEY_STR_USER_ID, strUserId).apply() + } + fun setCarNo(strCarNo:String) { + preferences.edit().putString(KEY_STR_CAR_NO, strCarNo).apply() + } + + fun getUserId(): String? { + return preferences.getString(KEY_STR_USER_ID, null) + } + fun getCarNo(): String? { + return preferences.getString(KEY_STR_CAR_NO, null) + } + + fun isLoggedIn(): Boolean { + return preferences.getBoolean(KEY_IS_LOGGED_IN, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/interfaces/API_FindPath.kt b/app/src/main/java/com/example/smart119/interfaces/API_FindPath.kt new file mode 100644 index 0000000..4302229 --- /dev/null +++ b/app/src/main/java/com/example/smart119/interfaces/API_FindPath.kt @@ -0,0 +1,19 @@ +package com.example.smart119.interfaces + +import com.google.gson.JsonObject +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.QueryMap + +interface apiFindPath { + @GET("/findPath") + fun findPath( + @QueryMap params: Map + ) : Call + + @GET("/getAddr") + fun getAddr( + @QueryMap params: Map + ) : Call +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/interfaces/API_Hospital.kt b/app/src/main/java/com/example/smart119/interfaces/API_Hospital.kt new file mode 100644 index 0000000..ac8764a --- /dev/null +++ b/app/src/main/java/com/example/smart119/interfaces/API_Hospital.kt @@ -0,0 +1,16 @@ +package com.example.smart119.interfaces + +import com.google.gson.JsonObject +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST + +interface apiHospital { + @POST("hospitalDetail.do") + fun getDetail( + @Body requestBody: JsonObject + ) : Call + + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/interfaces/API_Location.kt b/app/src/main/java/com/example/smart119/interfaces/API_Location.kt new file mode 100644 index 0000000..335f8c1 --- /dev/null +++ b/app/src/main/java/com/example/smart119/interfaces/API_Location.kt @@ -0,0 +1,58 @@ +package com.example.smart119.interfaces + +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.POST +import com.google.gson.JsonObject +import retrofit2.http.Body + +interface apiLocation { + @POST("/selectSidoList.do") + fun getSidoList() : Call + + @POST("/selectSiGunGuList.do") + fun getSiGunGuList( + @Body requestBody: JsonObject + ) : Call + + //body + //SIDO_CODE + //SI_GUN_GU_CODE + @POST("/selectEubMyeonDongList.do") + fun getEubMyeonDongList( + @Body requestBody: JsonObject + ) : Call + + @POST("/radiusSearchHospital2.do") + fun getAroundHospitalList( + @Body requestBody: JsonObject + ) : Call + + @POST("/searchHospital.do") + fun getAddressHospitalList( + @Body requestBody: JsonObject + ) : Call + + + /* + //유연하게 사용할 때 아래와 같이 사용 + @GET("path/to/endpoint") + + //param 날리는 방식 1 + fun getSiGunGuList( + @QueryMap options: Map + ): Call + + + //param 날리는 방식 2 + fun getSiGunGuList( + @Query("SIDO_CODE") param1: String + ) : Call + + val params = mapOf( + "key1" to "value1", + "key2" to "123" + ) + RetrofitClient.apiService.getSiGunGuList(params).enqueue(/* Callback */) + */ +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/interfaces/API_Login.kt b/app/src/main/java/com/example/smart119/interfaces/API_Login.kt new file mode 100644 index 0000000..189d869 --- /dev/null +++ b/app/src/main/java/com/example/smart119/interfaces/API_Login.kt @@ -0,0 +1,13 @@ +package com.example.smart119.interfaces + +import com.google.gson.JsonObject +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST + +interface apiLogin { + @POST("mobileLogin.do") + fun mLogin( + @Body requestBody: JsonObject + ) : Call +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/interfaces/API_Transfer.kt b/app/src/main/java/com/example/smart119/interfaces/API_Transfer.kt new file mode 100644 index 0000000..1971943 --- /dev/null +++ b/app/src/main/java/com/example/smart119/interfaces/API_Transfer.kt @@ -0,0 +1,33 @@ +package com.example.smart119.interfaces + +import com.google.gson.JsonObject +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST + +interface apiTransfer { + @POST("startEmergency.do") + fun startEMG( + @Body requestBody: JsonObject + ) : Call + + @POST("updateETDHospId.do") + fun changeHospital( + @Body requestBody: JsonObject + ) : Call + + @POST("updateETDDuration.do") + fun updateHospETA( + @Body requestBody: JsonObject + ) : Call + + @GET("endEmergency.do") + fun endEMG() : Call + + @POST("cookie") + fun cookieSend() : Call + + @GET("currentInAction.do") + fun currentAction() : Call +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/interfaces/CookieInterCeptor.kt b/app/src/main/java/com/example/smart119/interfaces/CookieInterCeptor.kt new file mode 100644 index 0000000..d3ba983 --- /dev/null +++ b/app/src/main/java/com/example/smart119/interfaces/CookieInterCeptor.kt @@ -0,0 +1,43 @@ +package com.example.smart119.interfaces + +import com.example.smart119.MyCookieJar +import okhttp3.Interceptor +import okhttp3.Response + +object CookieInterCeptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + var request = chain.request() + + // 요청 헤더에서 Cookie 확인 + val cookies = request.header("Cookie") + + println("Request URL: ${request.url()}") + if (cookies != null) { + println("Cookies in Request: $cookies") + } else { + println("No cookies in Request.") + +// val cookies = listOf( +// "sessionId=abc123", +// "csrfToken=xyz456", +// "userId=789" +// ) + val cookies = MyCookieJar.getCustomCookies() + + if(cookies.size > 0) { + request = request.newBuilder() +// .addHeader("Cookie", "sessionId=abc123") + .addHeader("Cookie", cookies.joinToString("; ")) // 수동으로 쿠키 추가 + .build() + } + + } + +// request = request.newBuilder() +// .addHeader("Cookie", "sessionId=abc123") +//// .addHeader("Cookie", cookies.joinToString("; ")) // 수동으로 쿠키 추가 +// .build() + + return chain.proceed(request) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/interfaces/RetrofitClient.kt b/app/src/main/java/com/example/smart119/interfaces/RetrofitClient.kt new file mode 100644 index 0000000..2fe3685 --- /dev/null +++ b/app/src/main/java/com/example/smart119/interfaces/RetrofitClient.kt @@ -0,0 +1,166 @@ +package com.example.smart119.interfaces + +import com.example.smart119.MyCookieJar +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import okhttp3.Cookie +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import javax.net.ssl.* +import java.util.concurrent.TimeUnit + +object RetrofitClient { + private const val HOST_IP_TEST = "192.168.0.20" + private const val HOST_PORT_TEST = "8080" + private const val BASE_URL_HTTP_TEST = "http://${HOST_IP_TEST}:${HOST_PORT_TEST}" + + //#region 테스트용 ip + + + //#region 일반 백엔드 api 설정 + private const val HOST_IP = "112.219.147.186" + private const val HOST_PORT = "8085" + private const val BASE_URL = "http://${HOST_IP}:${HOST_PORT}" + //#endregion + //#region nginx url 설정 + private const val NGINX_PORT = "1000" + private const val NGINX_BASE_URL = "http://${HOST_IP}:${NGINX_PORT}" + //#endregion + //#endregion + + /* + //#region 소방본부 서버용 ip + //#region 일반 백엔드 api 설정 + private const val HOST_IP = "210.104.143.125" + private const val HOST_PORT = "8085" + private const val BASE_URL = "http://${HOST_IP}:${HOST_PORT}" + //#endregion + //#region nginx url 설정 + private const val NGINX_IP = "210.104.143.125" + private const val NGINX_PORT = "1000" + private const val NGINX_BASE_URL = "http://${NGINX_IP}:${NGINX_PORT}" + //#endregion + //http://210.104.143.125:8085/ + //#endregion + */ + + private const val BASE_URL_HTTP = "http://${HOST_IP}:${HOST_PORT}" + private const val HTTP_URL_COOKIE = "http://192.168.0.20:3000" + + // 신뢰할 수 없는 인증서를 허용하는 OkHttpClient + private fun getUnsafeOkHttpClient(): OkHttpClient { + return try { + val trustAllCerts = arrayOf(object : X509TrustManager { + override fun checkClientTrusted(chain: Array, authType: String) {} + override fun checkServerTrusted(chain: Array, authType: String) {} + override fun getAcceptedIssuers(): Array = arrayOf() + }) + + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, trustAllCerts, java.security.SecureRandom()) + val sslSocketFactory = sslContext.socketFactory + + OkHttpClient.Builder() + .sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager) + .hostnameVerifier { _, _ -> true } // 모든 호스트 이름 허용 + .addInterceptor(CookieInterCeptor) + .connectTimeout(10, TimeUnit.SECONDS) // 서버 연결 Timeout (기본 10초 → 30초) + .readTimeout(10, TimeUnit.SECONDS) // 서버 응답 Timeout (기본 10초 → 30초) + .writeTimeout(10, TimeUnit.SECONDS) // 서버 요청 Timeout (기본 10초 → 30초) + .build() + } catch (e: Exception) { + throw RuntimeException(e) + } + } + + private fun getUnsafeOkHttpClient_http(): OkHttpClient { + return try { + OkHttpClient.Builder() + .addInterceptor(CookieInterCeptor) + .connectTimeout(10, TimeUnit.SECONDS) // 서버 연결 Timeout (기본 10초 → 30초) + .readTimeout(10, TimeUnit.SECONDS) // 서버 응답 Timeout (기본 10초 → 30초) + .writeTimeout(10, TimeUnit.SECONDS) // 서버 요청 Timeout (기본 10초 → 30초) + .build() + } catch (e: Exception) { + throw RuntimeException(e) + } + } + + private val retrofit: Retrofit by lazy { + Retrofit.Builder() + .baseUrl(BASE_URL) +// .client(getUnsafeOkHttpClient()) // 신뢰할 수 없는 인증서 허용 https + .client(getUnsafeOkHttpClient_http()) + .addConverterFactory(GsonConverterFactory.create()) // Gson Converter 추가 + .build() + } + + private val retrofit_NGINX: Retrofit by lazy { + Retrofit.Builder() + .baseUrl(NGINX_BASE_URL) + .client(getUnsafeOkHttpClient()) // 신뢰할 수 없는 인증서 허용 + .addConverterFactory(GsonConverterFactory.create()) // Gson Converter 추가 + .build() + } + + private val retrofit_HTTP: Retrofit by lazy { + Retrofit.Builder() + .baseUrl(BASE_URL_HTTP) + .client(getUnsafeOkHttpClient()) // 신뢰할 수 없는 인증서 허용 + .addConverterFactory(GsonConverterFactory.create()) // Gson Converter 추가 + .build() + } + + private val retrofit_HTTP_TEST: Retrofit by lazy { + Retrofit.Builder() + .baseUrl(BASE_URL_HTTP_TEST) + .client(getUnsafeOkHttpClient()) // 신뢰할 수 없는 인증서 허용 + .addConverterFactory(GsonConverterFactory.create()) // Gson Converter 추가 + .build() + } + + //cookie 테스트용 retrofit + private val retrofit_COOKIE: Retrofit by lazy { + Retrofit.Builder() + .baseUrl(HTTP_URL_COOKIE) + .client(getUnsafeOkHttpClient()) // 신뢰할 수 없는 인증서 허용 + .addConverterFactory(GsonConverterFactory.create()) // Gson Converter 추가 + .build() + } + + val locInstance: apiLocation by lazy { + retrofit.create(apiLocation::class.java) + } + + val loginInstance: apiLogin by lazy { + retrofit.create(apiLogin::class.java) +// retrofit_HTTP.create(apiLogin::class.java) +// retrofit_HTTP_TEST.create(apiLogin::class.java) + } + + val transferInstance: apiTransfer by lazy { + retrofit.create(apiTransfer::class.java) +// retrofit_HTTP.create(apiTransfer::class.java) +// retrofit_HTTP_TEST.create(apiTransfer::class.java) + } + + val pathInstance: apiFindPath by lazy { + retrofit_NGINX.create(apiFindPath::class.java) + } + +// val cookieInstance: apiTransfer by lazy { +// retrofit_COOKIE.create(apiTransfer::class.java) +// } + + val hospitalInstance:apiHospital by lazy { + retrofit.create(apiHospital::class.java) + } + + fun parseJsonFromBuffer(buffer: ByteArray): JsonObject { + val jsonString = buffer.toString(Charsets.UTF_8) + val jsonElement = JsonParser.parseString(jsonString) + return jsonElement.asJsonObject + } + +} diff --git a/app/src/main/java/com/example/smart119/viewModel/TransferViewModel.kt b/app/src/main/java/com/example/smart119/viewModel/TransferViewModel.kt new file mode 100644 index 0000000..89ae3f4 --- /dev/null +++ b/app/src/main/java/com/example/smart119/viewModel/TransferViewModel.kt @@ -0,0 +1,24 @@ +package com.example.smart119.viewModel + +import android.security.identity.ResultData +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch + +class TransferViewModel: ViewModel() { + private val _someLiveData = MutableLiveData() + val someLiveData:LiveData = _someLiveData + + fun requestData() { + viewModelScope.launch { +// try { +// val result = TransferRepository.getData() +// _someLiveData.value = result +// } catch (e: Exception) { +// // 에러 처리 +// } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart119/views/AddressSearchFragment.kt b/app/src/main/java/com/example/smart119/views/AddressSearchFragment.kt new file mode 100644 index 0000000..4907e8a --- /dev/null +++ b/app/src/main/java/com/example/smart119/views/AddressSearchFragment.kt @@ -0,0 +1,690 @@ +package com.example.smart119.views + +import android.graphics.Typeface +import android.os.Bundle +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.StyleSpan +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.Button +import android.widget.Spinner +import android.widget.TextView +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.NavHostFragment +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.example.smart119.API_Helper +import com.example.smart119.AlertDialogUtil +import com.example.smart119.ListItem +import com.example.smart119.LoginManager +import com.example.smart119.MedicalInfo +import com.example.smart119.R +import com.example.smart119.SpreadsheetAdapter +import com.example.smart119.TransferManager +import com.example.smart119.TransferState +import com.example.smart119.datas.HospitalSearch +import com.example.smart119.datas.SaveLogin +import com.example.smart119.interfaces.RetrofitClient +import com.google.android.material.bottomnavigation.BottomNavigationView +import com.google.android.material.textfield.TextInputEditText +import com.google.gson.JsonObject +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.util.* + +class AddressSearchFragment : Fragment() { + + private lateinit var asAdapter: SpreadsheetAdapter + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.address_search_page, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + FragmentHandler.registerView("AddressSearchFragment", view) + + hospitalInit(view); + HospitalSearch.clearAllData() + + //#region 광역시/도 콤보박스 설정 + val siItems = mutableListOf(HospitalSearch.GET_SIDO_DEFAULT_NAME()) + val siSpinner: Spinner = view.findViewById(R.id.siSelectSpinner) + val arrAdapterSi = + ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, siItems) + siSpinner.adapter = arrAdapterSi; + //#endregion +// //#region 시/군/구 콤보박스 설정 +// val gunGuItems = HospitalSearch.getGunGuNameList() +// val guGunSpinner: Spinner = view.findViewById(R.id.guGunSelectSpinner) +// val arrAdapterGuGun = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, gunGuItems) +// guGunSpinner.adapter = arrAdapterGuGun; +// //#endregion + + val searchButtonAS = view.findViewById + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml new file mode 100644 index 0000000..e416e1c --- /dev/null +++ b/app/src/main/res/layout/content_main.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/data_row.xml b/app/src/main/res/layout/data_row.xml new file mode 100644 index 0000000..5981f2c --- /dev/null +++ b/app/src/main/res/layout/data_row.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/popup_detail.xml b/app/src/main/res/layout/popup_detail.xml new file mode 100644 index 0000000..a82ac25 --- /dev/null +++ b/app/src/main/res/layout/popup_detail.xml @@ -0,0 +1,47 @@ + + + + + + + + + +