cyberman0306 / cardTalk

init
0 stars 0 forks source link

메모 #1

Open cyberman0306 opened 9 months ago

cyberman0306 commented 9 months ago

import SwiftUI

struct OverlappingViews: View { var body: some View { ZStack(alignment: .top) { // Title1 텍스트 Text("Title1") .font(.largeTitle) .padding() .background(Color.blue) .cornerRadius(10) .foregroundColor(.white) .shadow(radius: 5) .zIndex(1) // 이 뷰가 상단에 오도록 Z 인덱스 설정

        // Rectangle 뷰 겹치기
        Rectangle()
            .fill(Color.gray.opacity(0.5)) // 반투명한 회색으로 설정
            .frame(height: 50)
            .cornerRadius(10)
            .offset(y: 5) // Title1 뒤에 5 포인트 아래로 내리기
            .zIndex(0) // 이 뷰가 아래에 오도록 Z 인덱스 설정

        // 추가 뷰 겹치기 (예시)
        ForEach(1..<5) { index in
            Rectangle()
                .fill(Color.green.opacity(0.5)) // 반투명한 녹색으로 설정
                .frame(height: 50)
                .cornerRadius(10)
                .offset(x: CGFloat(index * 5), y: CGFloat(index * 5))
        }
    }
    .padding()
}

}

struct ContentView: View { var body: some View { OverlappingViews() } }

@main struct OverlappingApp: App { var body: some Scene { WindowGroup { ContentView() } } }

cyberman0306 commented 9 months ago

import SwiftUI

// Custom Text Modifier 정의 struct CustomTextStyle: ViewModifier { var fontSize: CGFloat var fontWeight: Font.Weight var foregroundColor: Color

func body(content: Content) -> some View {
    content
        .font(.system(size: fontSize, weight: fontWeight))
        .foregroundColor(foregroundColor)
}

}

// ViewModifier를 쉽게 적용하기 위한 Text의 extension extension Text { func customStyle(fontSize: CGFloat, fontWeight: Font.Weight, foregroundColor: Color) -> some View { self.modifier(CustomTextStyle(fontSize: fontSize, fontWeight: fontWeight, foregroundColor: foregroundColor)) } }

// 사용 예 struct ContentView: View { var body: some View { VStack { Text("Hello, World!") .customStyle(fontSize: 24, fontWeight: .bold, foregroundColor: .blue)

        Text("SwiftUI is awesome!")
            .customStyle(fontSize: 20, fontWeight: .medium, foregroundColor: .green)
    }
}

}

@main struct ModifierApp: App { var body: some Scene { WindowGroup { ContentView() } } }

cyberman0306 commented 8 months ago

죄송합니다, 제가 오해했습니다. 코르도바에서 URL을 전송하고, 다른 앱에서 해당 URL을 받아서 처리하는 방법에 대한 정확한 설명을 드리지 못했습니다. 코르도바 앱(A)에서 다른 앱(B)으로 URL을 보내고, 앱 B에서 그 URL을 받아 내장 웹뷰로 열어주는 과정을 다시 정리해 보겠습니다.

앱 A (코르도바 앱)에서 URL 전송

  1. 커스텀 URL 스킴 설정: 앱 B가 특정 URL 스킴을 가지고 있어야 합니다. 예를 들어, myapp:// 스킴을 앱 B가 처리할 수 있도록 설정해야 합니다. 이는 앱 B의 AndroidManifest.xml에 인텐트 필터를 추가하여 구현할 수 있습니다.

  2. 코르도바에서 URL 전송: 앱 A에서는 JavaScript를 사용하여 커스텀 URL 스킴을 가진 인텐트를 발생시키는 방식으로 앱 B를 열 수 있습니다. 예를 들어, window.open('myapp://some/path?query=parameter', '_system')를 호출하여 앱 B를 열고 데이터를 전송할 수 있습니다.

앱 B (코르도바 앱이 아닌 다른 앱)에서 URL 처리

앱 B를 Jetpack Compose를 사용한 안드로이드 앱으로 구성하고, 전송받은 URL을 내장 웹뷰에서 열도록 하겠습니다.

  1. 인텐트 필터 설정: 앱 B의 AndroidManifest.xml에 커스텀 URL 스킴을 처리할 수 있는 인텐트 필터를 추가합니다.

    <activity android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
            <data android:scheme="myapp" />
        </intent-filter>
    </activity>
  2. Compose와 함께 내장 WebView 사용: 앱 B의 MainActivity에서 전달받은 URL을 WebView를 통해 로드합니다. Jetpack Compose에서는 AndroidView를 사용하여 기존 WebView를 래핑할 수 있습니다.

    import android.os.Bundle
    import androidx.activity.ComponentActivity
    import androidx.activity.compose.setContent
    import androidx.compose.runtime.Composable
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.viewinterop.AndroidView
    import android.webkit.WebView
    import android.webkit.WebViewClient
    
    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val url = intent?.data?.toString() ?: "https://example.com"
    
            setContent {
                WebViewComponent(url)
            }
        }
    }
    
    @Composable
    fun WebViewComponent(url: String) {
        AndroidView(
            factory = { context ->
                WebView(context).apply {
                    webViewClient = WebViewClient()
                    loadUrl(url)
                }
            },
            modifier = Modifier.fillMaxSize()
        )
    }

이 구현을 통해 코르도바 앱(A)에서 window.open을 사용해 URL을 보내면, 해당 URL이 앱 B에서 내장 웹뷰를 통해 열리게 됩니다.

cyberman0306 commented 8 months ago

package com.example.browserapp

import android.annotation.SuppressLint import android.os.Bundle import android.webkit.WebView import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView

class MainActivity : ComponentActivity() { @SuppressLint("SetJavaScriptEnabled") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val urlToLoad = intent?.data?.toString() ?: "https://www.example.com"

    setContent {
        BrowserView(url = urlToLoad)
    }
}

}

@Composable fun BrowserView(url: String) { AndroidView( factory = { context -> WebView(context).apply { settings.javaScriptEnabled = true loadUrl(url) } }, modifier = Modifier.fillMaxSize() ) }

cyberman0306 commented 8 months ago

아래는 외부 링크를 받아 처리하는 간단한 Jetpack Compose 앱의 전체 코드 예시입니다. 이 예시에서는 앱이 인텐트로 전달받은 URL을 Custom Tabs를 사용하여 열도록 설정되어 있습니다. Custom Tabs를 사용하면 사용자에게 원활한 브라우징 경험을 제공할 수 있으며, 기본 브라우저의 룩앤필을 유지하면서도 앱 내에서 웹 콘텐츠를 표시할 수 있습니다.

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.mycomposeapp">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.MyComposeApp">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="https" />
                <data android:scheme="http" />
            </intent-filter>
        </activity>
    </application>

</manifest>

MainActivity.kt

package com.example.mycomposeapp

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.setContent

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val url = intent?.data ?: Uri.parse("https://www.example.com")

        // 인텐트로 전달받은 URL을 Custom Tabs를 사용하여 열기
        openUrlInCustomTabs(url)

        // 기본 UI 설정
        setContent {
            DefaultUI()
        }
    }

    private fun openUrlInCustomTabs(url: Uri) {
        val customTabsIntent = CustomTabsIntent.Builder().build()
        customTabsIntent.launchUrl(this, url)
    }
}

@Composable
fun DefaultUI() {
    MaterialTheme {
        Surface {
            Text(text = "Welcome to My Compose App")
        }
    }
}

build.gradle (Module)

Custom Tabs를 사용하기 위해, androidx.browser:browser 라이브러리를 의존성에 추가합니다.

dependencies {
    implementation 'androidx.core:core-ktx:1.6.0'
    implementation 'androidx.appcompat:appcompat:1.3.1'
    implementation 'com.google.android.material:material:1.4.0'
    implementation 'androidx.compose.ui:ui:1.0.1'
    implementation 'androidx.compose.material:material:1.0.1'
    implementation 'androidx.compose.ui:ui-tooling-preview:1.0.1'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
    implementation 'androidx.activity:activity-compose:1.3.1'
    implementation 'androidx.browser:browser:1.3.0'
}

이 코드는 앱이 시작될 때 인텐트로 전달받은 URL을 Custom Tabs를 사용하여 외부에서 열도록 합니다. 만약 URL이 인텐트로 전달되지 않는다면, 기본 URL(https://www.example.com)을 사용합니다. 또한, 간단한 텍스트를 표시하는 기본 UI를 제공합니다.

위의 코드 예시는 앱이 외부 링크를 처리하는 방법을 보여줍니다. 실제 앱에서는 사용자의 요구사항에 맞게 이 코드를 조정하거나 확장할 수 있습니다.

cyberman0306 commented 8 months ago

화면의 방향이 바뀌었을 때 새로고침을 방지하고 싶다면, 화면 방향 변경 시 액티비티의 재생성을 막아야 합니다. 안드로이드에서는 화면 방향 변경 시 기본적으로 액티비티가 재생성되어 앱의 상태가 초기화되는데, 이를 AndroidManifest.xml에 설정을 추가함으로써 막을 수 있습니다.

AndroidManifest.xml에 설정 추가하기

AndroidManifest.xml 파일 내 해당 액티비티 태그에 android:configChanges 속성을 추가하여, 화면 방향 변경(orientation)과 화면 크기 변경(screenSize)에 대해 시스템이 액티비티를 재생성하지 않도록 할 수 있습니다.

<activity
    android:name=".MainActivity"
    android:configChanges="orientation|screenSize|screenLayout|keyboardHidden">
    <!-- 나머지 액티비티 설정 -->
</activity>

이 설정은 화면의 방향이 바뀌거나, 화면 크기가 바뀌었을 때(예: 멀티 윈도우 모드에서의 크기 변경) 액티비티가 재생성되는 것을 막습니다. 여기서 screenLayoutkeyboardHidden은 필요에 따라 추가할 수 있는 옵션이며, 키보드 접근성이 변경되거나 화면 레이아웃이 바뀔 때도 액티비티 재생성을 막습니다.

onConfigurationChanged 메소드 오버라이드하기

android:configChanges에서 지정한 설정이 변경될 때, 시스템은 액티비티의 onConfigurationChanged 메소드를 호출합니다. 필요하다면 이 메소드를 오버라이드하여 화면 방향 변경에 따른 특정 동작을 수행할 수 있습니다. 다만, 이 설정을 사용할 때는 액티비티가 자동으로 재생성되지 않으므로, 화면 방향이나 크기가 변경되었을 때 필요한 UI 업데이트를 수동으로 관리해야 합니다.

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    // 화면 방향이나 크기가 바뀌었을 때 필요한 처리 수행
}

이 방법을 통해 화면 방향 변경 시 새로고침되는 문제를 해결할 수 있지만, 화면의 레이아웃과 리소스를 다양한 화면 방향과 크기에 맞게 적절히 조정해야 할 수도 있습니다. 따라서, 이 기능을 사용할 때는 앱의 사용성과 UI 디자인에 주의를 기울여야 합니다.

cyberman0306 commented 8 months ago

앱 B가 이미 실행 중일 때 앱 A에서 앱 B로 다시 URL을 보내는 시나리오에서, 앱 B가 새로운 URL 정보를 어떻게 처리할지는 앱 B의 구현에 달려 있습니다. onCreate() 메서드는 액티비티의 생명주기에서 처음으로 호출되며, 액티비티가 처음 생성될 때 한 번만 실행됩니다. 따라서 이미 실행 중인 액티비티에 대해 onCreate()가 다시 호출되지 않는 이상, 여기서 URL을 처리하는 로직은 새로운 인텐트를 처리할 수 없습니다.

새로운 인텐트 처리하기

앱 B가 이미 실행 중일 때 새로운 인텐트(예: 새 URL)를 받아야 한다면, onNewIntent(Intent intent) 메서드를 오버라이드하여 새 인텐트를 처리할 수 있습니다. 이 메서드는 시스템이 액티비티에 새 인텐트를 전달할 때 호출됩니다.

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    // 새 인텐트에서 URL 처리
    val newUrl = intent?.data.toString()
    // 새 URL로 무언가를 업데이트하는 로직
}

액티비티 속성 설정

onNewIntent가 호출되도록 하려면, 액티비티가 새 인텐트를 받을 준비가 되어 있어야 합니다. 이를 위해 액티비티의 launchModesingleTop, singleTask 또는 singleInstance로 설정해야 할 수 있습니다. 이러한 모드는 액티비티 인스턴스에 대한 정책을 정의하며, 새 인텐트가 도착했을 때 기존 인스턴스를 재사용할지 결정합니다.

예를 들어, AndroidManifest.xml에서 액티비티를 singleTop으로 설정하면, 액티비티가 이미 태스크의 맨 위에 있을 때 새 인텐트가 해당 액티비티로 전달됩니다.

<activity
    android:name=".MainActivity"
    android:launchMode="singleTop">
    <!-- 인텐트 필터 등 기타 설정 -->
</activity>

요약

cyberman0306 commented 8 months ago

package com.example.myapp

import android.content.Intent import android.net.Uri import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext

class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)

    setContent {
        val context = LocalContext.current
        val urlState = remember { mutableStateOf(intent?.data?.toString() ?: "No URL") }

        // 여기서 Compose UI를 구성합니다.
        // 예: Text(text = urlState.value)
        // URL 상태를 UI에 표시하거나 다른 용도로 사용할 수 있습니다.
    }
}

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    // 새 인텐트에서 URL 처리
    val newUrl = intent?.data.toString()
    // 새 URL로 무언가를 업데이트하는 로직
    // 예를 들어, URL 상태를 업데이트하거나 UI를 다시 렌더링할 수    // 새 URL로 상태를 업데이트하는 로직 추가
// 예시 코드의 이 부분을 추가하세요

// 상태를 저장하는 변수를 액티비티 레벨에 선언합니다.
private val urlState = mutableStateOf("No URL")

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // 초기 인텐트에서 URL을 설정합니다.
    urlState.value = intent?.data?.toString() ?: "No URL"

    setContent {
        val context = LocalContext.current

        // Compose 내부에서 urlState를 사용합니다.
        // Composable 함수 내부에서 urlState.value를 참조하여 UI를 업데이트 합니다.
        DisplayUrl(url = urlState.value)
    }
}

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    // 새 인텐트에서 URL 처리
    val newUrl = intent?.data?.toString()
    // 상태 업데이트
    urlState.value = newUrl ?: "No URL"
}

}

@Composable fun DisplayUrl(url: String) { // 여기서 url 파라미터를 사용하여 URL을 표시합니다. Text(text = "Current URL: $url") }

cyberman0306 commented 8 months ago

package com.example.myapp

import android.content.Intent import android.net.Uri import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext

class MainActivity : ComponentActivity() { private val urlState = mutableStateOf("No URL")

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    urlState.value = intent?.data?.toString() ?: "No URL"

    setContent {
        DisplayUrl(url = urlState.value)
    }
}

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    val newUrl = intent?.data.toString()
    urlState.value = newUrl ?: "No URL"
}

}

@Composable fun DisplayUrl(url: String) { Text(text = "Current URL: $url") }

cyberman0306 commented 8 months ago

package com.example.myapp

import android.content.Intent import android.net.Uri import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalContext

class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 초기 인텐트에서 URL 가져오기 val initialUrl = intent?.data?.toString() ?: "https://www.example.com"

    setContent {
        // remember로 상태를 저장하고 LaunchedEffect로 onNewIntent에서 변경된 URL을 처리
        val currentUrl = rememberSaveable { mutableStateOf(initialUrl) }

        LaunchedEffect(Unit) {
            currentUrl.value = intent?.data?.toString() ?: "https://www.example.com"
        }

        BrowserView(url = currentUrl.value)
    }
}

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    // 새 인텐트에서 URL 업데이트
    val newUrl = intent?.data?.toString() ?: "https://www.example.com"
    setContent {
        BrowserView(url = newUrl)
    }
}

}

@Composable fun BrowserView(url: String) { MaterialTheme { Surface { Text(text = url) } } }

cyberman0306 commented 8 months ago
class MainViewModel : ViewModel() {
    private val _url = MutableLiveData("https://www.example.com")
    val url: LiveData<String> = _url

    fun setUrl(newUrl: String) {
        _url.value = newUrl
    }
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        handleIntent(intent)

        setContent {
            val viewModel: MainViewModel by viewModels()
            BrowserScreen(viewModel = viewModel)
        }
    }

    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        handleIntent(intent)
    }

    private fun handleIntent(intent: Intent?) {
        val newUrl = intent?.data?.toString() ?: "https://www.example.com"
        val viewModel: MainViewModel by viewModels()
        viewModel.setUrl(newUrl)
    }
}

@Composable
fun BrowserScreen(viewModel: MainViewModel) {
    val url = viewModel.url.observeAsState(initial = "https://www.example.com")
    BrowserView(url = url.value)
}

@Composable
fun BrowserView(url: String) {
    Text(text = "Current URL: $url")
}

이 코드는 MainActivity에서 새 인텐트를 받을 때마다 handleIntent 메서드를 호출하여 MainViewModel의 URL 상태를 업데이트합니다. BrowserScreen 컴포저블은 이 URL 상태를 observeAsState를 사용하여 관찰하고, 상태 변화에 따라 UI를 업데이트합니다. BrowserView는 단순히 현재 URL을 표시하는 예시로 사용되었습니다. 실제 앱에서는 이 부분을 적절한 웹 컨텐츠 렌더링 또는 다른 UI 요소로 대체할 수 있습니다.

cyberman0306 commented 8 months ago

observeAsState는 Jetpack Compose에서 LiveData (또는 StateFlow, SharedFlow 등)를 관찰(observe)하고, 그 값을 Compose 함수 내에서 상태(state)로 사용할 수 있게 해주는 확장 함수입니다. observeAsState를 사용하면 LiveData가 업데이트될 때마다 Compose 함수가 자동으로 재구성(recomposition)되어 UI가 최신 상태를 반영하게 됩니다.

제공된 예시 코드에서 observeAsState 함수의 사용이 누락되었거나 명확히 언급되지 않았다면, 이는 코드 설명 과정에서의 실수일 수 있습니다. LiveData를 사용하여 Compose UI에서 상태를 관찰하려면, 아래와 같이 observeAsState를 사용할 수 있습니다.

예시 코드에서 observeAsState 사용하기

먼저, 아래와 같은 종속성을 추가하여 프로젝트 설정에서 Compose와 LiveData를 함께 사용할 수 있도록 해야 합니다.

implementation "androidx.compose.runtime:runtime-livedata:$compose_version"

그런 다음, Compose UI 내에서 LiveData를 관찰하기 위해 observeAsState를 사용할 수 있습니다.

@Composable
fun BrowserScreen(viewModel: MainViewModel) {
    // viewModel.url은 LiveData<String>입니다.
    // observeAsState()를 사용하여 LiveData의 현재 값을 Compose 상태로 변환합니다.
    val url = viewModel.url.observeAsState(initial = "https://www.example.com")

    BrowserView(url = url.value)
}

이 코드에서 observeAsState() 함수는 viewModel.url LiveData 객체를 관찰하고, 이 LiveData 객체의 현재 값(url.value)을 BrowserView 컴포저블 함수에 전달합니다. initial 매개변수는 LiveData 객체가 아직 초기값을 발행하지 않았을 때 사용할 초기값을 지정합니다.

observeAsState는 Compose 함수가 LiveData 또는 StateFlow의 값 변화에 따라 자동으로 업데이트되도록 합니다. 이를 통해 선언적 UI 패러다임에 따라 UI와 데이터 상태 간의 동기화를 간편하게 관리할 수 있습니다.

cyberman0306 commented 8 months ago
class MainViewModel : ViewModel() {
    val urlStateFlow = MutableStateFlow("https://www.example.com")
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val viewModel: MainViewModel by viewModels()

        handleIntent(intent)

        setContent {
            BrowserScreen(viewModel)
        }
    }

    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        handleIntent(intent)
    }

    private fun handleIntent(intent: Intent?) {
        val viewModel: MainViewModel by viewModels()
        val newUrl = intent?.data?.toString() ?: "https://www.example.com"
        viewModel.urlStateFlow.value = newUrl
    }
}

@Composable
fun BrowserScreen(viewModel: MainViewModel) {
    val url by viewModel.urlStateFlow.collectAsState()
    BrowserView(url = url)
}

@Composable
fun BrowserView(url: String) {
    Text(text = "Current URL: $url")
}

이 코드는 StateFlow를 사용하여 URL 상태를 MainViewModel에서 관리하고, MainActivity에서 새 인텐트를 받을 때마다 이 상태를 업데이트합니다. BrowserScreen 컴포저블 함수는 viewModel로부터 URL 상태를 collectAsState()를 사용하여 관찰하고, BrowserView에서 이를 표시합니다.

이 방식은 Jetpack Compose와 함께 StateFlow를 사용하여 앱의 상태를 효율적으로 관리하고 UI를 데이터에 반응적으로 만들 수 있는 좋은 예시입니다.

cyberman0306 commented 8 months ago

Android에서 실제 웹사이트를 표시하기 위해 WebView를 사용할 수 있습니다. Jetpack Compose와 함께 WebView를 사용하려면, AndroidView 컴포저블을 활용해야 합니다. 이를 통해 기존 Android 뷰 시스템의 WebView를 Compose 내에서 사용할 수 있습니다.

아래 코드는 BrowserView 컴포저블을 수정하여, 전달받은 URL로 웹사이트를 로드하고 표시하는 WebView를 포함하는 예시입니다. WebView의 기본 설정을 적용하고, JavaScript가 활성화되어 있어야 웹 콘텐츠가 올바르게 표시될 수 있습니다.

BrowserView 컴포저블 수정하기

@Composable
fun BrowserView(url: String) {
    AndroidView(factory = { context ->
        WebView(context).apply {
            webViewClient = WebViewClient()
            settings.javaScriptEnabled = true
            loadUrl(url)
        }
    }, modifier = Modifier.fillMaxSize())
}

이 코드는 AndroidView를 사용하여 Compose UI 내에 WebView를 생성하고, 이 WebView를 사용하여 전달받은 URL의 웹사이트를 로드하고 표시합니다. factory 람다 내에서 WebView 인스턴스를 생성하고 초기화하며, Modifier.fillMaxSize()를 사용하여 WebView가 가능한 많은 공간을 차지하도록 합니다.

주의사항

이 예시는 Jetpack Compose를 사용하여 실제 URL을 로드하고 표시하는 간단한 브라우저 뷰를 구현하는 방법을 보여줍니다.

cyberman0306 commented 8 months ago

아래는 singleInstance 런치 모드를 사용하고, onNewIntent를 오버라이드하여 새 URL을 처리하는 안드로이드 앱의 전체 코드 예입니다. 이 예제에서는 MainActivity가 URL을 처리하고 WebView를 통해 해당 URL을 로드합니다.

class MainActivity : ComponentActivity() {
    private lateinit var webView: WebView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            AndroidView(factory = { context ->
                WebView(context).also {
                    webView = it
                    it.webViewClient = WebViewClient()
                    it.settings.javaScriptEnabled = true
                    // 앱이 처음 시작될 때 전달받은 URL을 로드합니다.
                    intent?.data?.toString()?.let { url ->
                        it.loadUrl(url)
                    }
                }
            })
        }
    }

    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        // 새 인텐트로부터 URL을 받아 WebView에 로드합니다.
        val newUrl = intent?.data?.toString()
        newUrl?.let {
            webView.loadUrl(it)
        }
    }
}

이 코드에서는 MainActivityonCreate 메소드에서 AndroidView를 사용하여 WebView를 초기화하고 설정합니다. webViewClientWebViewClient()로 설정하고, JavaScript를 활성화합니다. 앱이 처음 시작될 때 전달받은 인텐트로부터 URL을 가져와 WebView에 로드합니다.

또한, onNewIntent 메소드를 오버라이드하여 새로운 인텐트가 전달될 때마다 그 안의 URL을 WebView에 로드하도록 합니다. 이렇게 하면 singleInstance 모드에서도 앱이 다른 URL로의 요청을 적절히 처리할 수 있습니다.

이 구현은 singleInstance 런치 모드와 onNewIntent 메소드를 사용하여 앱이 실행 중일 때 전달받은 새 URL을 WebView에 로드하는 방법을 보여줍니다.

cyberman0306 commented 8 months ago

주어진 요구사항에 따라 ViewModel을 사용하여 StateFlow로 URL 상태를 관리하고, MainActivity에서 이 상태를 관찰하여 WebView에 새 URL을 로드하는 코드의 전체 구현은 다음과 같습니다:

MainViewModel.kt

class MainViewModel : ViewModel() {
    private val _urlStateFlow = MutableStateFlow("https://www.example.com")
    val urlStateFlow: StateFlow<String> = _urlStateFlow

    fun setUrl(newUrl: String) {
        _urlStateFlow.value = newUrl
    }
}

MainActivity.kt

class MainActivity : ComponentActivity() {
    private lateinit var webView: WebView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val viewModel: MainViewModel by viewModels()

        viewModel.setUrl(intent?.data?.toString() ?: "https://www.example.com")

        setContent {
            val url by viewModel.urlStateFlow.collectAsState()
            AndroidView(factory = { context ->
                WebView(context).apply {
                    webView = this
                    webViewClient = WebViewClient()
                    settings.javaScriptEnabled = true
                }
            }, modifier = Modifier.fillMaxSize())

            LaunchedEffect(url) {
                webView.loadUrl(url)
            }
        }
    }

    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        val viewModel: MainViewModel by viewModels()
        intent?.data?.toString()?.let {
            viewModel.setUrl(it)
        }
    }
}

이 코드는 앱이 실행 중일 때 전달된 새 인텐트에 포함된 URL을 처리하고, 해당 URL을 WebView에 로드하여 표시합니다. MainViewModel에서는 MutableStateFlow를 사용하여 URL 상태를 관리하고, MainActivity에서는 LaunchedEffect를 사용하여 URL 상태의 변화를 감지하고 WebView에 새 URL을 로드합니다.

이 구현은 다음 요구사항을 충족합니다:

이 코드는 코틀린과 Jetpack Compose를 사용하는 안드로이드 앱 개발에 있어 현대적인 접근 방식을 반영합니다.

cyberman0306 commented 8 months ago

주어진 요구사항에 따라 ViewModel을 사용하여 StateFlow로 URL 상태를 관리하고, MainActivity에서 이 상태를 관찰하여 WebView에 새 URL을 로드하는 코드의 전체 구현은 다음과 같습니다:

MainViewModel.kt

class MainViewModel : ViewModel() {
    private val _urlStateFlow = MutableStateFlow("https://www.example.com")
    val urlStateFlow: StateFlow<String> = _urlStateFlow

    fun setUrl(newUrl: String) {
        _urlStateFlow.value = newUrl
    }
}

MainActivity.kt

class MainActivity : ComponentActivity() {
    private lateinit var webView: WebView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val viewModel: MainViewModel by viewModels()

        viewModel.setUrl(intent?.data?.toString() ?: "https://www.example.com")

        setContent {
            val url by viewModel.urlStateFlow.collectAsState()
            AndroidView(factory = { context ->
                WebView(context).apply {
                    webView = this
                    webViewClient = WebViewClient()
                    settings.javaScriptEnabled = true
                }
            }, modifier = Modifier.fillMaxSize())

            LaunchedEffect(url) {
                webView.loadUrl(url)
            }
        }
    }

    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        val viewModel: MainViewModel by viewModels()
        intent?.data?.toString()?.let {
            viewModel.setUrl(it)
        }
    }
}

이 코드는 앱이 실행 중일 때 전달된 새 인텐트에 포함된 URL을 처리하고, 해당 URL을 WebView에 로드하여 표시합니다. MainViewModel에서는 MutableStateFlow를 사용하여 URL 상태를 관리하고, MainActivity에서는 LaunchedEffect를 사용하여 URL 상태의 변화를 감지하고 WebView에 새 URL을 로드합니다.

이 구현은 다음 요구사항을 충족합니다:

이 코드는 코틀린과 Jetpack Compose를 사용하는 안드로이드 앱 개발에 있어 현대적인 접근 방식을 반영합니다.

cyberman0306 commented 8 months ago

customsviewer:// 스킴을 사용하여 특정 웹사이트, 예를 들어 "http://www.naver.com"을 앱 B에서 열기 위해서는, 커스텀 URL 스킴에 이 웹사이트를 인코딩하여 포함시키거나, 앱 내에서 커스텀 스킴을 해석하여 해당 웹사이트로 리다이렉트하는 로직을 구현해야 합니다. 하지만 직접적으로 http://www.naver.comcustomsviewer:// 스킴으로 변경하는 것은 실제 웹사이트 주소를 바로 열 수 있는 방법이 아닙니다. 대신, 앱 B가 customsviewer:// URL을 받았을 때, 내부적으로 http://www.naver.com으로 리다이렉트하도록 구현할 수 있습니다.

AndroidManifest.xml 수정

<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <!-- customsviewer 스킴으로 시작하는 URL만 처리 -->
        <data android:scheme="customsviewer" android:host="www.naver.com" />
    </intent-filter>
</activity>

앱 B에서 URL 처리

MainActivity 내부에서 인텐트를 처리하여 customsviewer:// 스킴으로 시작하는 URL을 받았을 때, 실제로 http://www.naver.comWebView 등을 사용하여 열도록 구현해야 합니다.

MainActivity.kt 수정 예시

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val intentUri = intent?.data

        if (intentUri?.scheme == "customsviewer" && intentUri.host == "www.naver.com") {
            // WebView를 사용하여 http://www.naver.com 열기
            // 예제에서는 단순화를 위해 setContent만 사용
            setContent {
                WebView(context = LocalContext.current).apply {
                    webViewClient = WebViewClient()
                    loadUrl("http://www.naver.com")
                }
            }
        }
    }
}

위의 예제에서 customsviewer://www.naver.com 형식의 URL을 앱 A에서 앱 B로 호출하면, 앱 B는 MainActivity에서 이를 받아 실제로 http://www.naver.com을 로드하도록 처리합니다. 이 과정에서 실제 웹 주소를 WebView를 통해 열 수 있습니다.

cyberman0306 commented 8 months ago

네, 커스텀 URL 스킴을 사용하여 앱 간에 데이터를 전달할 때 인코딩/디코딩 과정 없이 직접 데이터를 전달하는 것이 가능합니다. 하지만 URL이나 데이터에 특수 문자나 스페이스 등이 포함되어 있을 경우, 이를 올바르게 전달하기 위해 인코딩/디코딩을 권장합니다. 그러나 단순한 데이터 전달이라면, 인코딩/디코딩 과정 없이도 데이터를 전달하고 처리할 수 있습니다.

커스텀 스킴을 사용하여 앱 B에서 특정 URL을 직접 처리하고 싶다면, 앱 B의 AndroidManifest.xml에 정의된 인텐트 필터에 해당 스킴을 추가하고, 앱 A에서 해당 스킴으로 URL을 전송하면 됩니다. 여기서 중요한 것은 앱 B가 인텐트를 올바르게 인식하고 처리할 수 있도록 AndroidManifest.xml과 관련 액티비티의 로직을 구성하는 것입니다.

예를 들어, 앱 B가 customsviewer:// 스킴으로 시작하는 URL을 처리하도록 설정하고, http://www.example.com과 같은 웹사이트를 열기 위해 직접적으로 전달하고 싶다면, 다음과 같이 구성할 수 있습니다:

AndroidManifest.xml (앱 B)

<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <!-- customsviewer 스킴 처리 -->
        <data android:scheme="customsviewer" />
    </intent-filter>
</activity>

앱 A에서 앱 B 호출 (예: 코르도바 또는 다른 앱)

window.open('customsviewer://www.example.com', '_system');

MainActivity.kt (앱 B)

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val webView = remember { WebView(LocalContext.current) }
            AndroidView(factory = { webView }) { webView ->
                webView.webViewClient = WebViewClient()

                // 인텐트에서 URI 스킴을 기반으로 URL을 가져옵니다.
                val uri = intent?.data
                val url = uri?.toString()?.replace("customsviewer://", "http://")

                // URL을 로드합니다.
                if (url != null) {
                    webView.loadUrl(url)
                }
            }
        }
    }
}

이 예제에서는 customsviewer://www.example.com 형식의 URL이 전달되면, 이를 http://www.example.com으로 변환하여 WebView에서 로드합니다. 데이터 전달 시 인코딩/디코딩을 사용하지 않고 직접 URL을 전달하고 처리하는 방식입니다.

단, 실제 URL 처리 로직은 앱의 요구사항에 맞게 조정해야 하며, 전달되는 데이터의 형식과 안전성을 고려하여 적절한 처리가 필요합니다.

cyberman0306 commented 8 months ago

물론입니다. onNewIntent(Intent intent) 함수는 이미 실행 중인 액티비티가 새로운 인텐트를 받았을 때 호출됩니다. 이 함수는 singleTop, singleTask, 또는 singleInstance 런치 모드를 사용하는 액티비티에서 주로 사용됩니다. 새 인텐트를 통해 전달된 데이터를 처리하기 위해 onNewIntent 내에서 로직을 구현할 수 있습니다.

앱 B가 customsviewer:// 스킴으로 시작하는 URL을 처리하고, onNewIntent 메소드를 사용하여 실행 중인 인스턴스에서 새 URL을 받을 때 이를 적절히 처리하는 예시는 다음과 같습니다:

MainActivity.kt 수정 예시 (앱 B)


override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    // 새 인텐트로부터 URI를 받아옵니다.
    val newUri = intent?.data

    // 새 URI가 있다면, 해당 URI로 WebView를 업데이트합니다.
    newUri?.let {
        val webView = findViewById<WebView>(R.id.webView) // WebView 참조를 가져옵니다.
        webView
cyberman0306 commented 8 months ago

앱 A에서 앱 B를 호출할 때 앱 B가 설치되어 있지 않아 발생할 수 있는 에러를 관리하는 방법은 다음과 같습니다. Android에서는 이러한 상황을 처리하기 위해 try-catch 블록을 사용하여 ActivityNotFoundException을 캐치할 수 있습니다. 하지만 코르도바 또는 웹뷰 기반 앱에서는 자바스크립트를 통해 직접적으로 이러한 예외를 처리할 수 없습니다. 대신, 앱 B가 설치되어 있는지 미리 확인하거나, 에러 발생 시 사용자에게 적절한 피드백을 제공하는 방법을 사용할 수 있습니다.

앱 A에서 앱 B의 설치 여부 확인하기

앱 A에서 앱 B를 호출하기 전에 앱 B가 기기에 설치되어 있는지 확인하려면, 코르도바 플러그인이나 네이티브 코드를 사용해야 합니다. 예를 들어, Android 네이티브 코드를 사용하는 경우, 다음과 같은 방법으로 특정 패키지가 설치되어 있는지 확인할 수 있습니다:

fun isAppInstalled(packageName: String, context: Context): Boolean {
    return try {
        context.packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
        true
    } catch (e: PackageManager.NameNotFoundException) {
        false
    }
}

이 함수를 사용하여 앱 B의 설치 여부를 확인한 후, 설치되어 있지 않다면 사용자에게 앱 설치를 유도하는 메시지를 표시할 수 있습니다.

코르도바에서의 처리 방법

코르도바에서는 특정 앱의 설치 여부를 확인하기 위한 기본적인 기능을 제공하지 않습니다. 따라서, 앱 B의 설치 여부를 확인하기 위해서는 커스텀 플러그인을 작성하거나, 기존에 사용 가능한 플러그인(예: cordova-plugin-appavailability)을 사용해야 합니다.

에러 발생 시 사용자에게 메시지를 표시하는 방법은, 앱 설치 여부 확인 후 설치되어 있지 않은 경우, 알림, 팝업, 또는 사용자가 앱 B를 설치할 수 있는 마켓으로 리다이렉트하는 방식 등 다양하게 구현할 수 있습니다.

예시: 사용자에게 피드백 제공

if (isAppInstalled('앱 B의 패키지명')) {
    window.open('customsviewer://some/path', '_system');
} else {
    // 앱 B가 설치되어 있지 않음을 사용자에게 알림
    alert('앱 B가 설치되어 있지 않습니다. 설치 페이지로 이동합니다.');
    window.open('마켓 URL 또는 앱 B 설치 페이지', '_system');
}

위 예시에서 isAppInstalled 함수는 앱의 설치 여부를 확인하는 가상의 함수로, 실제로는 해당 기능을 수행하는 코르도바 플러그인의 API를 호출해야 합니다. 사용자에게 알림을 제공하는 방법은 애플리케이션의 UX 디자인과 요구사항에 따라 달라질 수 있습니다.

cyberman0306 commented 4 months ago

import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.Image import androidx.compose.foundation.layout. import androidx.compose.material. import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp

class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MyApp() } } }

@Composable fun MyApp() { Scaffold( topBar = { TopSection() }, content = { MiddleSection() }, bottomBar = { BottomSection() } ) }

@Composable fun TopSection() { TopAppBar( title = { Text("정부대전청사의 식단", fontSize = 20.sp) }, actions = { IconButton(onClick = { /TODO/ }) { Icon(painter = painterResource(id = R.drawable.ic_info), contentDescription = "Info") } IconButton(onClick = { /TODO/ }) { Icon(painter = painterResource(id = R.drawable.ic_share), contentDescription = "Share") } }, backgroundColor = Color.White, contentColor = Color.Black ) }

@Composable fun MiddleSection() { Column( modifier = Modifier .fillMaxSize() .padding(16.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { Image( painter = painterResource(id = R.drawable.weekly_meal_plan), contentDescription = "주간 식단표", modifier = Modifier.fillMaxWidth() ) } }

@Composable fun BottomSection() { BottomAppBar( backgroundColor = Color.White, contentColor = Color.Black ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly ) { TextButton(onClick = { /TODO/ }) { Text("1동, 2동") } TextButton(onClick = { /TODO/ }) { Text("3동, 4동") } TextButton(onClick = { /TODO/ }) { Text("양식당") } TextButton(onClick = { /TODO/ }) { Text("후생동 식당") } } } }

cyberman0306 commented 4 months ago

Jetpack Compose에서 URL로 특정 사이트에 접속해 내부 이미지 URL을 가져와 화면에 표시하는 방법을 설명드리겠습니다. 이 작업은 네트워크 작업과 이미지를 화면에 표시하는 두 가지 주요 단계를 포함합니다.

1. 네트워크 작업 (이미지 URL 가져오기)

먼저 Retrofit 라이브러리를 사용하여 네트워크 요청을 통해 이미지 URL을 가져오는 방법을 구현합니다.

2. Jetpack Compose로 이미지 표시하기

네트워크 작업을 통해 가져온 URL을 이용하여 이미지를 Jetpack Compose에서 표시합니다.

1단계: 네트워크 작업 설정

1. Retrofit 및 Gson 설치

build.gradle 파일에 다음 종속성을 추가합니다:

dependencies {
    implementation "com.squareup.retrofit2:retrofit:2.9.0"
    implementation "com.squareup.retrofit2:converter-gson:2.9.0"
    implementation "com.squareup.okhttp3:logging-interceptor:4.9.1"
    implementation "io.coil-kt:coil-compose:1.3.2"
}

2. Retrofit 설정

Retrofit 인터페이스와 서비스 클래스를 설정합니다:

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path

data class ImageResponse(val imageUrl: String)

interface ApiService {
    @GET("path/to/your/image")
    suspend fun getImageUrl(): ImageResponse
}

object RetrofitInstance {
    val api: ApiService by lazy {
        Retrofit.Builder()
            .baseUrl("https://yourapi.baseurl.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ApiService::class.java)
    }
}

2단계: Jetpack Compose에서 이미지 표시

이미지를 Jetpack Compose 화면에 표시합니다:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import coil.compose.rememberImagePainter
import kotlinx.coroutines.launch
import retrofit2.HttpException
import java.io.IOException

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Surface(modifier = Modifier.fillMaxSize()) {
                    DisplayImageFromUrl()
                }
            }
        }
    }
}

@Composable
fun DisplayImageFromUrl() {
    var imageUrl by remember { mutableStateOf("") }
    val coroutineScope = rememberCoroutineScope()

    LaunchedEffect(Unit) {
        coroutineScope.launch {
            try {
                val response = RetrofitInstance.api.getImageUrl()
                imageUrl = response.imageUrl
            } catch (e: IOException) {
                // Handle IO exception
            } catch (e: HttpException) {
                // Handle HTTP exception
            }
        }
    }

    if (imageUrl.isNotEmpty()) {
        Image(
            painter = rememberImagePainter(imageUrl),
            contentDescription = null,
            modifier = Modifier.fillMaxSize()
        )
    }
}

요약

  1. Retrofit을 사용하여 네트워크 요청으로 이미지 URL을 가져옵니다.
  2. Jetpack Compose를 사용하여 이미지 URL을 화면에 표시합니다.

이 예제는 기본적인 설정을 포함하고 있으며, 실제 환경에서는 오류 처리나 UI 상태 관리와 같은 추가적인 작업이 필요할 수 있습니다.

cyberman0306 commented 4 months ago

implementation("io.coil-kt:coil-compose:2.7.0") implementation("io.coil-kt:coil:2.7.0")

cyberman0306 commented 3 months ago

import androidx.compose.material. import androidx.compose.runtime. import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch

@Composable fun MainScreen() { val scaffoldState = rememberScaffoldState() val scope = rememberCoroutineScope()

Scaffold(
    scaffoldState = scaffoldState,
    topBar = {
        TopAppBar(
            title = { Text("My App") },
            navigationIcon = {
                IconButton(onClick = {
                    scope.launch {
                        scaffoldState.drawerState.open()
                    }
                }) {
                    Icon(
                        imageVector = ImageVector.vectorResource(id = R.drawable.ic_menu),
                        contentDescription = "Menu"
                    )
                }
            }
        )
    },
    drawerContent = {
        Column(
            modifier = Modifier.fillMaxSize()
        ) {
            Text("Drawer Item 1", modifier = Modifier.padding(16.dp))
            Text("Drawer Item 2", modifier = Modifier.padding(16.dp))
            // Add more drawer items here
        }
    },
    content = { paddingValues ->
        // 메인 컨텐츠
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues)
        ) {
            Text("Main content goes here")
        }
    }
)

}

@Preview(showBackground = true) @Composable fun PreviewMainScreen() { MaterialTheme { Surface { MainScreen() } } }

cyberman0306 commented 3 months ago

Compose를 사용하여 HTML을 파싱하고 특정 div 안에 있는 이미지 주소를 가져오는 기능을 구현하기 위해 KotlinJSoup 라이브러리를 사용할 수 있습니다. JSoup은 HTML 파싱에 매우 유용하며, Kotlin과 함께 사용하여 필요한 정보를 추출할 수 있습니다.

다음은 예제 코드입니다:

  1. 프로젝트에 JSoup 라이브러리를 추가합니다. build.gradle 파일에 다음 종속성을 추가합니다:
dependencies {
    implementation 'org.jsoup:jsoup:1.14.3'
}
  1. Kotlin 코드에서 JSoup을 사용하여 HTML을 파싱하고 이미지를 추출합니다:
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jsoup.Jsoup

fun fetchHtml(url: String): String? {
    return try {
        val response = Jsoup.connect(url).get()
        response.html()
    } catch (e: Exception) {
        e.printStackTrace()
        null
    }
}

fun parseHtml(html: String): List<String> {
    val imageUrls = mutableListOf<String>()
    val document = Jsoup.parse(html)
    val div = document.select("div.specific-div-class") // 적절한 div 클래스 이름으로 변경
    if (div.isNotEmpty()) {
        val images = div.select("img")
        for (img in images) {
            val imageUrl = img.attr("src")
            imageUrls.add(imageUrl)
        }
    }
    return imageUrls
}

fun main() {
    val url = "https://gbmo.go.kr/chungsa/dv/dietView/selectDietCalendarView.do?mi=1277&gbd=CD002"
    CoroutineScope(Dispatchers.IO).launch {
        val html = fetchHtml(url)
        if (html != null) {
            val imageUrls = parseHtml(html)
            withContext(Dispatchers.Main) {
                imageUrls.forEach { imageUrl ->
                    println(imageUrl)
                }
            }
        } else {
            println("Failed to retrieve the webpage.")
        }
    }
}

코드 설명:

  1. fetchHtml 함수는 지정된 URL에서 HTML 콘텐츠를 가져옵니다.
  2. parseHtml 함수는 JSoup을 사용하여 HTML을 파싱하고, 특정 div 안에 있는 이미지 URL을 추출합니다. div의 클래스 이름을 실제 HTML 구조에 맞게 변경해야 합니다.
  3. main 함수는 코루틴을 사용하여 비동기적으로 HTML을 가져오고 파싱된 이미지 URL을 출력합니다.

이 예시에서는 specific-div-class를 실제 HTML 구조에 맞는 div 클래스 이름으로 바꿔야 합니다. 이를 통해 해당 div 안에 있는 모든 이미지 URL을 추출할 수 있습니다.

Compose 환경에서도 이 코드를 사용하여 필요한 데이터를 가져온 후 UI를 업데이트할 수 있습니다.

cyberman0306 commented 3 months ago

만약 Compose 프로젝트에서 gradle.kts 파일에 JSoup을 추가하고 싶다면 다음과 같은 방법으로 설정할 수 있습니다.

Step 1: build.gradle.kts 설정

  1. 프로젝트의 build.gradle.kts 파일을 열고, dependencies 블록에 JSoup 라이브러리를 추가합니다.
dependencies {
    implementation("org.jsoup:jsoup:1.14.3")
}
  1. Coroutine 라이브러리가 필요하다면, 다음과 같이 추가합니다:
dependencies {
    implementation("org.jsoup:jsoup:1.14.3")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
}

Step 2: Kotlin 코드 작성

Compose 프로젝트 내에서 JSoupCoroutine을 사용하여 HTML을 파싱하고 이미지를 추출하는 코드를 작성합니다.

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.BasicText
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import kotlinx.coroutines.*
import org.jsoup.Jsoup
import org.jsoup.nodes.Document

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp()
        }
    }
}

@Composable
fun MyApp() {
    var imageUrls by remember { mutableStateOf(listOf<String>()) }

    LaunchedEffect(Unit) {
        withContext(Dispatchers.IO) {
            val urls = fetchImageUrls("https://gbmo.go.kr/chungsa/dv/dietView/selectDietCalendarView.do?mi=1277&gbd=CD002")
            imageUrls = urls
        }
    }

    MaterialTheme {
        Surface(modifier = Modifier.fillMaxSize()) {
            LazyColumn {
                items(imageUrls.size) { index ->
                    BasicText(text = imageUrls[index])
                }
            }
        }
    }
}

suspend fun fetchImageUrls(url: String): List<String> {
    return withContext(Dispatchers.IO) {
        val html = fetchHtml(url)
        if (html != null) {
            parseHtml(html)
        } else {
            emptyList()
        }
    }
}

fun fetchHtml(url: String): String? {
    return try {
        val response = Jsoup.connect(url).get()
        response.html()
    } catch (e: Exception) {
        e.printStackTrace()
        null
    }
}

fun parseHtml(html: String): List<String> {
    val imageUrls = mutableListOf<String>()
    val document: Document = Jsoup.parse(html)
    val div = document.select("div.specific-div-class") // 적절한 div 클래스 이름으로 변경
    if (div.isNotEmpty()) {
        val images = div.select("img")
        for (img in images) {
            val imageUrl = img.attr("src")
            imageUrls.add(imageUrl)
        }
    }
    return imageUrls
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MyApp()
}

코드 설명:

  1. MainActivity에서 setContent를 사용하여 MyApp 컴포저블을 설정합니다.
  2. MyApp 컴포저블에서는 LaunchedEffect를 사용하여 비동기적으로 HTML을 가져와 이미지를 추출하고 상태를 업데이트합니다.
  3. fetchImageUrls 함수는 주어진 URL에서 이미지를 가져오는 코루틴 함수입니다.
  4. fetchHtml 함수는 JSoup을 사용하여 HTML 콘텐츠를 가져옵니다.
  5. parseHtml 함수는 HTML을 파싱하여 특정 div 안에 있는 이미지 URL을 추출합니다.
  6. LazyColumn을 사용하여 이미지를 리스트로 표시합니다.

이 코드를 사용하면 Compose 앱에서 특정 웹 페이지의 이미지를 비동기적으로 가져와 화면에 표시할 수 있습니다. specific-div-class를 실제 HTML 구조에 맞는 div 클래스 이름으로 바꾸는 것을 잊지 마세요.

cyberman0306 commented 3 months ago

Compose와 JSoup을 사용하여 특정 select 요소 내의 모든 option 태그를 추출하는 방법을 예시와 함께 설명하겠습니다.

Step 1: build.gradle.kts 설정

프로젝트의 build.gradle.kts 파일을 열고, dependencies 블록에 JSoup 라이브러리를 추가합니다.

dependencies {
    implementation("org.jsoup:jsoup:1.14.3")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
}

Step 2: Kotlin 코드 작성

Compose 프로젝트 내에서 JSoupCoroutine을 사용하여 HTML을 파싱하고 select 요소 내의 모든 option 태그를 추출합니다.

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.BasicText
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import kotlinx.coroutines.*
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp()
        }
    }
}

@Composable
fun MyApp() {
    var options by remember { mutableStateOf(listOf<String>()) }

    LaunchedEffect(Unit) {
        withContext(Dispatchers.IO) {
            val optionsList = fetchOptions("https://gbmo.go.kr/chungsa/dv/dietView/selectDietCalendarView.do?mi=1277&gbd=CD002")
            options = optionsList
        }
    }

    MaterialTheme {
        Surface(modifier = Modifier.fillMaxSize()) {
            LazyColumn {
                items(options.size) { index ->
                    BasicText(text = options[index])
                }
            }
        }
    }
}

suspend fun fetchOptions(url: String): List<String> {
    return withContext(Dispatchers.IO) {
        val html = fetchHtml(url)
        if (html != null) {
            parseHtml(html)
        } else {
            emptyList()
        }
    }
}

fun fetchHtml(url: String): String? {
    return try {
        val response = Jsoup.connect(url).get()
        response.html()
    } catch (e: Exception) {
        e.printStackTrace()
        null
    }
}

fun parseHtml(html: String): List<String> {
    val options = mutableListOf<String>()
    val document: Document = Jsoup.parse(html)
    val selectElement: Element? = document.select("select[name=gbd]").first()
    selectElement?.let {
        val optionElements = it.select("option")
        for (option in optionElements) {
            val optionText = option.text()
            options.add(optionText)
        }
    }
    return options
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MyApp()
}

코드 설명:

  1. MainActivity에서 setContent를 사용하여 MyApp 컴포저블을 설정합니다.
  2. MyApp 컴포저블에서는 LaunchedEffect를 사용하여 비동기적으로 HTML을 가져와 select 요소 내의 option 태그를 추출하고 상태를 업데이트합니다.
  3. fetchOptions 함수는 주어진 URL에서 option 태그를 가져오는 코루틴 함수입니다.
  4. fetchHtml 함수는 JSoup을 사용하여 HTML 콘텐츠를 가져옵니다.
  5. parseHtml 함수는 HTML을 파싱하여 특정 select 요소 내의 option 태그를 추출합니다. select[name=gbd]를 사용하여 name 속성이 gbdselect 요소를 선택합니다.
  6. LazyColumn을 사용하여 option 태그의 텍스트를 리스트로 표시합니다.

이 코드를 사용하면 Compose 앱에서 특정 웹 페이지의 select 요소 내의 모든 option 태그를 비동기적으로 가져와 화면에 표시할 수 있습니다.

cyberman0306 commented 3 months ago

물론입니다! 아래에 코드를 하나씩 설명하면서, Compose와 코루틴이 어떻게 동작하는지 설명하겠습니다.

전체 코드

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.BasicText
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import kotlinx.coroutines.*
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import android.util.Log

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp()
        }
    }
}

@Composable
fun MyApp() {
    var options by remember { mutableStateOf(listOf<String>()) }

    LaunchedEffect(Unit) {
        Log.d("MyApp", "LaunchedEffect entered")
        withContext(Dispatchers.IO) {
            val optionsList = fetchOptions("https://gbmo.go.kr/chungsa/dv/dietView/selectDietCalendarView.do?mi=1277&gbd=CD002")
            options = optionsList
            // print options for debugging
            optionsList.forEach {
                println(it)
                Log.d("Option", it)
            }
        }
    }

    MaterialTheme {
        Surface(modifier = Modifier.fillMaxSize()) {
            LazyColumn {
                items(options) { option ->
                    BasicText(text = option)
                }
            }
        }
    }
}

suspend fun fetchOptions(url: String): List<String> {
    return withContext(Dispatchers.IO) {
        val html = fetchHtml(url)
        if (html != null) {
            parseHtml(html)
        } else {
            emptyList()
        }
    }
}

fun fetchHtml(url: String): String? {
    return try {
        val response = Jsoup.connect(url).get()
        response.html()
    } catch (e: Exception) {
        e.printStackTrace()
        null
    }
}

fun parseHtml(html: String): List<String> {
    val options = mutableListOf<String>()
    val document: Document = Jsoup.parse(html)
    val selectElement: Element? = document.select("select[name=gbd]").first()
    selectElement?.let {
        val optionElements = it.select("option")
        for (option in optionElements) {
            val optionText = option.text()
            options.add(optionText)
        }
    }
    return options
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MyApp()
}

주요 부분 설명

MainActivity 클래스

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp()
        }
    }
}

MyApp Composable 함수

@Composable
fun MyApp() {
    var options by remember { mutableStateOf(listOf<String>()) }

    LaunchedEffect(Unit) {
        Log.d("MyApp", "LaunchedEffect entered")
        withContext(Dispatchers.IO) {
            val optionsList = fetchOptions("https://gbmo.go.kr/chungsa/dv/dietView/selectDietCalendarView.do?mi=1277&gbd=CD002")
            options = optionsList
            // print options for debugging
            optionsList.forEach {
                println(it)
                Log.d("Option", it)
            }
        }
    }

    MaterialTheme {
        Surface(modifier = Modifier.fillMaxSize()) {
            LazyColumn {
                items(options) { option ->
                    BasicText(text = option)
                }
            }
        }
    }
}

fetchOptions 함수

suspend fun fetchOptions(url: String): List<String> {
    return withContext(Dispatchers.IO) {
        val html = fetchHtml(url)
        if (html != null) {
            parseHtml(html)
        } else {
            emptyList()
        }
    }
}

fetchHtml 함수

fun fetchHtml(url: String): String? {
    return try {
        val response = Jsoup.connect(url).get()
        response.html()
    } catch (e: Exception) {
        e.printStackTrace()
        null
    }
}

parseHtml 함수

fun parseHtml(html: String): List<String> {
    val options = mutableListOf<String>()
    val document: Document = Jsoup.parse(html)
    val selectElement: Element? = document.select("select[name=gbd]").first()
    selectElement?.let {
        val optionElements = it.select("option")
        for (option in optionElements) {
            val optionText = option.text()
            options.add(optionText)
        }
    }
    return options
}

요약

이 코드는 Compose와 코루틴을 사용하여 웹 페이지에서 특정 <select> 요소 내의 모든 <option> 태그를 비동기적으로 가져와서 화면에 표시합니다. Compose는 UI를 선언적으로 작성할 수 있게 해주며, 코루틴은 비동기 작업을 간단하고 효율적으로 처리할 수 있게 해줍니다. Jsoup 라이브러리는 HTML 파싱을 쉽게 할 수 있도록 도와줍니다.

cyberman0306 commented 3 months ago

Compose에서 NavigationDrawerItem을 사용하여 List<String>Drawer 메뉴 항목으로 추가하려면, LazyColumn을 사용하여 각 항목을 NavigationDrawerItem으로 변환할 수 있습니다. 아래에 예제 코드를 통해 이를 구현하는 방법을 설명하겠습니다.

전체 코드

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.*
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp()
        }
    }
}

@Composable
fun MyApp() {
    val scaffoldState = rememberScaffoldState()
    val scope = rememberCoroutineScope()
    var options by remember { mutableStateOf(listOf<String>()) }

    LaunchedEffect(Unit) {
        withContext(Dispatchers.IO) {
            val optionsList = fetchOptions("https://gbmo.go.kr/chungsa/dv/dietView/selectDietCalendarView.do?mi=1277&gbd=CD002")
            options = optionsList
        }
    }

    Scaffold(
        scaffoldState = scaffoldState,
        topBar = {
            TopAppBar(
                title = { Text("My App") },
                navigationIcon = {
                    IconButton(onClick = {
                        scope.launch {
                            scaffoldState.drawerState.open()
                        }
                    }) {
                        Icon(Icons.Filled.Menu, contentDescription = "Menu")
                    }
                }
            )
        },
        drawerContent = {
            DrawerContent(options)
        }
    ) { paddingValues ->
        // Main content
        Box(modifier = Modifier.padding(paddingValues)) {
            Text("Main content goes here")
        }
    }
}

@Composable
fun DrawerContent(options: List<String>) {
    LazyColumn {
        items(options) { option ->
            NavigationDrawerItem(
                label = { Text(option) },
                selected = false,
                onClick = { /* TODO: Handle navigation */ },
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp)
            )
        }
    }
}

suspend fun fetchOptions(url: String): List<String> {
    return withContext(Dispatchers.IO) {
        val html = fetchHtml(url)
        if (html != null) {
            parseHtml(html)
        } else {
            emptyList()
        }
    }
}

fun fetchHtml(url: String): String? {
    return try {
        val response = Jsoup.connect(url).get()
        response.html()
    } catch (e: Exception) {
        e.printStackTrace()
        null
    }
}

fun parseHtml(html: String): List<String> {
    val options = mutableListOf<String>()
    val document: Document = Jsoup.parse(html)
    val selectElement: Element? = document.select("select[name=gbd]").first()
    selectElement?.let {
        val optionElements = it.select("option")
        for (option in optionElements) {
            val optionText = option.text()
            options.add(optionText)
        }
    }
    return options
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MyApp()
}

주요 부분 설명

MainActivity

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp()
        }
    }
}

MyApp Composable 함수

@Composable
fun MyApp() {
    val scaffoldState = rememberScaffoldState()
    val scope = rememberCoroutineScope()
    var options by remember { mutableStateOf(listOf<String>()) }

    LaunchedEffect(Unit) {
        withContext(Dispatchers.IO) {
            val optionsList = fetchOptions("https://gbmo.go.kr/chungsa/dv/dietView/selectDietCalendarView.do?mi=1277&gbd=CD002")
            options = optionsList
        }
    }

    Scaffold(
        scaffoldState = scaffoldState,
        topBar = {
            TopAppBar(
                title = { Text("My App") },
                navigationIcon = {
                    IconButton(onClick = {
                        scope.launch {
                            scaffoldState.drawerState.open()
                        }
                    }) {
                        Icon(Icons.Filled.Menu, contentDescription = "Menu")
                    }
                }
            )
        },
        drawerContent = {
            DrawerContent(options)
        }
    ) { paddingValues ->
        // Main content
        Box(modifier = Modifier.padding(paddingValues)) {
            Text("Main content goes here")
        }
    }
}

DrawerContent Composable 함수

@Composable
fun DrawerContent(options: List<String>) {
    LazyColumn {
        items(options) { option ->
            NavigationDrawerItem(
                label = { Text(option) },
                selected = false,
                onClick = { /* TODO: Handle navigation */ },
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp)
            )
        }
    }
}

fetchOptions 함수

suspend fun fetchOptions(url: String): List<String> {
    return withContext(Dispatchers.IO) {
        val html = fetchHtml(url)
        if (html != null) {
            parseHtml(html)
        } else {
            emptyList()
        }
    }
}

fetchHtml 함수

fun fetchHtml(url: String): String? {
    return try {
        val response = Jsoup.connect(url).get()
        response.html()
    } catch (e: Exception) {
        e.printStackTrace()
        null
    }
}

parseHtml 함수

fun parseHtml(html: String): List<String> {
    val options = mutableListOf<String>()
    val document: Document = Jsoup.parse(html)
    val selectElement: Element? = document.select("select[name=gbd]").first()
    selectElement?.let {
        val optionElements = it.select("option")
        for (option in optionElements) {
            val optionText = option.text()
            options.add(optionText)
        }
    }
    return options
}

Preview


@Preview(show