검증게시판 단계 추가
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.GoldenAnalysis"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
tools:targetApi="31">
|
||||
|
||||
<!-- Splash Activity -->
|
||||
<activity
|
||||
android:name=".ui.SplashActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.GoldenAnalysis.Splash">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Main Activity -->
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.GoldenAnalysis" />
|
||||
|
||||
<!-- WorkManager Initialization -->
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
<meta-data
|
||||
android:name="androidx.work.WorkManagerInitializer"
|
||||
android:value="androidx.startup" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.analysis.goldenanalysis.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.ui.AppBarConfiguration
|
||||
import androidx.navigation.ui.setupActionBarWithNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import com.analysis.goldenanalysis.R
|
||||
import com.analysis.goldenanalysis.databinding.ActivityMainBinding
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
try {
|
||||
Log.d("MainActivity", "onCreate started")
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
Log.d("MainActivity", "Layout inflated")
|
||||
|
||||
// Setup bottom navigation
|
||||
val navView: BottomNavigationView = binding.navView
|
||||
|
||||
Log.d("MainActivity", "Getting NavHostFragment")
|
||||
val navHostFragment = supportFragmentManager
|
||||
.findFragmentById(R.id.nav_host_fragment_activity_main) as? NavHostFragment
|
||||
|
||||
if (navHostFragment == null) {
|
||||
Log.e("MainActivity", "NavHostFragment not found!")
|
||||
Toast.makeText(this, "Navigation 초기화 실패", Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
val navController = navHostFragment.navController
|
||||
Log.d("MainActivity", "NavController obtained")
|
||||
|
||||
// Passing each menu ID as a set of Ids because each
|
||||
// menu should be considered as top level destinations.
|
||||
val appBarConfiguration = AppBarConfiguration(
|
||||
setOf(
|
||||
R.id.navigation_dashboard,
|
||||
R.id.navigation_portfolio,
|
||||
R.id.navigation_chart,
|
||||
R.id.navigation_alerts,
|
||||
R.id.navigation_scheduler,
|
||||
R.id.navigation_settings
|
||||
)
|
||||
)
|
||||
|
||||
try {
|
||||
setupActionBarWithNavController(navController, appBarConfiguration)
|
||||
navView.setupWithNavController(navController)
|
||||
Log.d("MainActivity", "Navigation setup complete")
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Error setting up navigation", e)
|
||||
// Continue without action bar
|
||||
navView.setupWithNavController(navController)
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Fatal error in onCreate", e)
|
||||
Toast.makeText(this, "앱 초기화 중 오류 발생: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.analysis.goldenanalysis.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.analysis.goldenanalysis.R
|
||||
|
||||
class SplashActivity : AppCompatActivity() {
|
||||
|
||||
private val splashTimeOut: Long = 2000 // 2 seconds
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
try {
|
||||
Log.d("SplashActivity", "onCreate started")
|
||||
setContentView(R.layout.activity_splash)
|
||||
Log.d("SplashActivity", "Layout set")
|
||||
|
||||
// Navigate to MainActivity after splash timeout
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
try {
|
||||
Log.d("SplashActivity", "Starting MainActivity")
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
} catch (e: Exception) {
|
||||
Log.e("SplashActivity", "Error starting MainActivity", e)
|
||||
}
|
||||
}, splashTimeOut)
|
||||
} catch (e: Exception) {
|
||||
Log.e("SplashActivity", "Error in onCreate", e)
|
||||
// Try to start MainActivity directly
|
||||
try {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
} catch (e2: Exception) {
|
||||
Log.e("SplashActivity", "Failed to recover", e2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package com.analysis.goldenanalysis.ui.alert
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.analysis.goldenanalysis.R
|
||||
import com.analysis.goldenanalysis.data.api.ApiClientFactory
|
||||
import com.analysis.goldenanalysis.data.model.AlertNotificationDto
|
||||
import com.analysis.goldenanalysis.databinding.FragmentAlertsBinding
|
||||
import com.analysis.goldenanalysis.databinding.ItemAlertPopupCardBinding
|
||||
import com.analysis.goldenanalysis.databinding.ItemAlertStockRowBinding
|
||||
import com.analysis.goldenanalysis.util.AlertMessageParser
|
||||
import com.analysis.goldenanalysis.util.AlertSignalInference
|
||||
import com.analysis.goldenanalysis.util.UpbitWebUrls
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* 미읽음 알림 — 프론트 AlertSnackbar와 유사한 카드 스택 UI
|
||||
*/
|
||||
class AlertsFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentAlertsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var cryptoMap: Map<String, String> = emptyMap()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
_binding = FragmentAlertsBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
|
||||
binding.toolbar.inflateMenu(R.menu.menu_alerts)
|
||||
binding.toolbar.setOnMenuItemClickListener(::onMenuItem)
|
||||
|
||||
binding.swipeRefresh.setOnRefreshListener { loadAll() }
|
||||
loadAll()
|
||||
}
|
||||
|
||||
private fun onMenuItem(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.action_all_notifications) {
|
||||
findNavController().navigate(R.id.action_alerts_to_notification_list)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun loadAll() {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
binding.progress.visibility = View.VISIBLE
|
||||
binding.tvEmpty.visibility = View.GONE
|
||||
try {
|
||||
val api = ApiClientFactory.alertApi(requireContext())
|
||||
val cryptos = withContext(Dispatchers.IO) {
|
||||
runCatching { api.getCryptoList() }.getOrElse { emptyList() }
|
||||
}
|
||||
cryptoMap = cryptos.associate { it.koreanName to it.symbol }
|
||||
val unread = withContext(Dispatchers.IO) {
|
||||
runCatching { api.getUnreadNotifications(null) }.getOrElse { emptyList() }
|
||||
}
|
||||
renderCards(unread)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(requireContext(), "알림 로드 실패: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
binding.tvEmpty.visibility = View.VISIBLE
|
||||
} finally {
|
||||
binding.progress.visibility = View.GONE
|
||||
binding.swipeRefresh.isRefreshing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderCards(notifications: List<AlertNotificationDto>) {
|
||||
binding.cardsContainer.removeAllViews()
|
||||
if (notifications.isEmpty()) {
|
||||
binding.tvEmpty.visibility = View.VISIBLE
|
||||
return
|
||||
}
|
||||
binding.tvEmpty.visibility = View.GONE
|
||||
val inflater = layoutInflater
|
||||
notifications.forEach { n ->
|
||||
val card = ItemAlertPopupCardBinding.inflate(inflater, binding.cardsContainer, false)
|
||||
val (title, pairs) = AlertMessageParser.titleAndPairs(n)
|
||||
card.tvCardTitle.text = title
|
||||
|
||||
card.btnClose.setOnClickListener {
|
||||
n.id?.let { id -> dismissNotification(id) }
|
||||
}
|
||||
card.btnInfo.setOnClickListener {
|
||||
n.id?.let { id ->
|
||||
SignalDetailBottomSheet.newInstance(
|
||||
notificationId = id,
|
||||
rowSymbol = n.symbol,
|
||||
signal = AlertSignalInference.inferFromNotification(n, null) ?: "BUY",
|
||||
).show(parentFragmentManager, "signal_detail")
|
||||
} ?: Toast.makeText(requireContext(), "알림 ID 없음", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
if (pairs.isNotEmpty()) {
|
||||
card.scrollRows.visibility = View.VISIBLE
|
||||
card.tvSingleLineMessage.visibility = View.GONE
|
||||
card.containerRows.removeAllViews()
|
||||
pairs.forEach { pair ->
|
||||
val row = ItemAlertStockRowBinding.inflate(inflater, card.containerRows, false)
|
||||
val resolved = AlertMessageParser.resolveStockStrategyPairRow(pair, cryptoMap)
|
||||
row.tvKoreanName.text = resolved.koreanName
|
||||
row.tvSymbol.text = resolved.displaySymbol
|
||||
|
||||
val marketSym = resolved.navSymbol
|
||||
val upbitUrl = UpbitWebUrls.getUpbitWebExchangeUrl(marketSym, n.timeInterval)
|
||||
|
||||
row.btnUpbit.setOnClickListener {
|
||||
if (upbitUrl != null) {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(upbitUrl)))
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "업비트 URL을 만들 수 없습니다.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
row.btnAlertList.setOnClickListener {
|
||||
openAlertListForSymbol(marketSym, resolved.koreanName, n)
|
||||
}
|
||||
row.root.setOnClickListener {
|
||||
openAlertListForSymbol(marketSym, resolved.koreanName, n)
|
||||
}
|
||||
|
||||
row.btnSignalDetail.setOnClickListener {
|
||||
val nid = n.id ?: return@setOnClickListener
|
||||
val sig = AlertSignalInference.inferFromNotification(n, resolved.koreanName) ?: "BUY"
|
||||
SignalDetailBottomSheet.newInstance(nid, marketSym, sig)
|
||||
.show(parentFragmentManager, "signal_detail")
|
||||
}
|
||||
|
||||
row.root.layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
)
|
||||
card.containerRows.addView(row.root)
|
||||
}
|
||||
} else {
|
||||
card.scrollRows.visibility = View.GONE
|
||||
card.tvSingleLineMessage.visibility = View.VISIBLE
|
||||
card.tvSingleLineMessage.text =
|
||||
n.message?.split("\n")?.firstOrNull()?.take(200).orEmpty()
|
||||
}
|
||||
|
||||
binding.cardsContainer.addView(card.root)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openAlertListForSymbol(
|
||||
marketSym: String?,
|
||||
koreanName: String,
|
||||
notification: AlertNotificationDto,
|
||||
) {
|
||||
val prefs = requireContext().getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||
marketSym?.let {
|
||||
prefs.edit()
|
||||
.putString("pending_chart_symbol", it)
|
||||
.putString("pending_chart_korean", koreanName)
|
||||
.apply()
|
||||
}
|
||||
findNavController().navigate(
|
||||
R.id.action_alerts_to_notification_list,
|
||||
bundleOf(
|
||||
"filterSymbol" to (marketSym ?: ""),
|
||||
"filterKorean" to koreanName,
|
||||
"highlightId" to (notification.id ?: 0L),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun dismissNotification(id: Long) {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
ApiClientFactory.alertApi(requireContext()).markAsRead(id)
|
||||
}
|
||||
loadAll()
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(requireContext(), "닫기 실패: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package com.analysis.goldenanalysis.ui.alert
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.analysis.goldenanalysis.data.model.AlertNotificationDto
|
||||
import com.analysis.goldenanalysis.databinding.ItemAlertStockRowBinding
|
||||
import com.analysis.goldenanalysis.databinding.ItemNotificationHistoryBinding
|
||||
import com.analysis.goldenanalysis.util.AlertMessageParser
|
||||
import com.analysis.goldenanalysis.util.AlertSignalInference
|
||||
|
||||
class NotificationHistoryAdapter(
|
||||
private val onUpbit: (symbol: String?, timeInterval: String?) -> Unit,
|
||||
private val onAlertListRow: (notification: AlertNotificationDto, symbol: String?, korean: String) -> Unit,
|
||||
private val onSignalDetail: (notificationId: Long, rowSymbol: String?, signal: String) -> Unit,
|
||||
private val onDelete: (Long) -> Unit,
|
||||
) : RecyclerView.Adapter<NotificationHistoryAdapter.VH>() {
|
||||
|
||||
private val items = mutableListOf<AlertNotificationDto>()
|
||||
private var filterSymbol: String = ""
|
||||
private var filterKorean: String = ""
|
||||
private var cryptoMap: Map<String, String> = emptyMap()
|
||||
|
||||
fun setFilter(symbol: String, korean: String) {
|
||||
filterSymbol = symbol.trim()
|
||||
filterKorean = korean.trim()
|
||||
}
|
||||
|
||||
fun submit(list: List<AlertNotificationDto>, map: Map<String, String>) {
|
||||
cryptoMap = map
|
||||
items.clear()
|
||||
val filtered = list.filter { n ->
|
||||
if (filterSymbol.isEmpty() && filterKorean.isEmpty()) return@filter true
|
||||
val msg = n.message.orEmpty()
|
||||
if (filterSymbol.isNotEmpty() && msg.contains(filterSymbol, ignoreCase = true)) return@filter true
|
||||
if (filterKorean.isNotEmpty() && msg.contains(filterKorean)) return@filter true
|
||||
if (filterSymbol.isNotEmpty() && n.symbol.equals(filterSymbol, ignoreCase = true)) return@filter true
|
||||
false
|
||||
}
|
||||
items.addAll(filtered)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
|
||||
val binding = ItemNotificationHistoryBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return VH(binding)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
inner class VH(private val binding: ItemNotificationHistoryBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(n: AlertNotificationDto) {
|
||||
val (title, pairs) = AlertMessageParser.titleAndPairs(n)
|
||||
binding.tvTitle.text = title
|
||||
binding.tvTime.text = n.triggeredAt ?: n.notifiedAt ?: ""
|
||||
|
||||
binding.btnDelete.setOnClickListener {
|
||||
n.id?.let { onDelete(it) }
|
||||
}
|
||||
|
||||
binding.containerRows.removeAllViews()
|
||||
if (pairs.isEmpty()) {
|
||||
binding.containerRows.visibility = View.GONE
|
||||
} else {
|
||||
binding.containerRows.visibility = View.VISIBLE
|
||||
val inflater = LayoutInflater.from(binding.root.context)
|
||||
pairs.forEach { pair ->
|
||||
val resolved = AlertMessageParser.resolveStockStrategyPairRow(pair, cryptoMap)
|
||||
val row = ItemAlertStockRowBinding.inflate(inflater, binding.containerRows, false)
|
||||
row.tvKoreanName.text = resolved.koreanName
|
||||
row.tvSymbol.text = resolved.displaySymbol
|
||||
val sym = resolved.navSymbol
|
||||
row.btnUpbit.setOnClickListener { onUpbit(sym, n.timeInterval) }
|
||||
row.btnAlertList.setOnClickListener {
|
||||
onAlertListRow(n, sym, resolved.koreanName)
|
||||
}
|
||||
row.root.setOnClickListener {
|
||||
onAlertListRow(n, sym, resolved.koreanName)
|
||||
}
|
||||
row.btnSignalDetail.setOnClickListener {
|
||||
val id = n.id ?: return@setOnClickListener
|
||||
val sig = AlertSignalInference.inferFromNotification(n, resolved.koreanName) ?: "BUY"
|
||||
onSignalDetail(id, sym, sig)
|
||||
}
|
||||
row.root.layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
)
|
||||
binding.containerRows.addView(row.root)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package com.analysis.goldenanalysis.ui.alert
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.analysis.goldenanalysis.R
|
||||
import com.analysis.goldenanalysis.data.api.ApiClientFactory
|
||||
import com.analysis.goldenanalysis.data.model.AlertNotificationDto
|
||||
import com.analysis.goldenanalysis.databinding.FragmentNotificationListBinding
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* 전체 알림 목록 (프론트 NotificationListPage 유사)
|
||||
*/
|
||||
class NotificationListFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentNotificationListBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private lateinit var adapter: NotificationHistoryAdapter
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
_binding = FragmentNotificationListBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
|
||||
binding.toolbar.setNavigationIcon(com.analysis.goldenanalysis.R.drawable.ic_arrow_back)
|
||||
binding.toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
binding.toolbar.title = getString(R.string.alerts_menu_all)
|
||||
|
||||
adapter = NotificationHistoryAdapter(
|
||||
onUpbit = { sym, interval ->
|
||||
val url = com.analysis.goldenanalysis.util.UpbitWebUrls.getUpbitWebExchangeUrl(sym, interval)
|
||||
if (url != null) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||
else Toast.makeText(requireContext(), "업비트 URL 오류", Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
onAlertListRow = { _, _, _ -> /* 목록 화면 내 동작 */ },
|
||||
onSignalDetail = { id, sym, signal ->
|
||||
SignalDetailBottomSheet.newInstance(id, sym, signal)
|
||||
.show(parentFragmentManager, "signal_detail")
|
||||
},
|
||||
onDelete = { id -> deleteNotification(id) },
|
||||
)
|
||||
binding.recycler.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.recycler.adapter = adapter
|
||||
|
||||
val filterSym = arguments?.getString("filterSymbol").orEmpty()
|
||||
val filterKr = arguments?.getString("filterKorean").orEmpty()
|
||||
adapter.setFilter(filterSym, filterKr)
|
||||
|
||||
binding.swipeRefresh.setOnRefreshListener { loadPage() }
|
||||
loadPage()
|
||||
}
|
||||
|
||||
private fun loadPage() {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
binding.progress.visibility = View.VISIBLE
|
||||
try {
|
||||
val api = ApiClientFactory.alertApi(requireContext())
|
||||
val (page, map) = withContext(Dispatchers.IO) {
|
||||
val p = runCatching { api.getNotifications(null, 0, 50) }.getOrNull()
|
||||
val m = runCatching { api.getCryptoList().associate { it.koreanName to it.symbol } }
|
||||
.getOrElse { emptyMap() }
|
||||
p to m
|
||||
}
|
||||
val list = page?.content.orEmpty()
|
||||
adapter.submit(list, map)
|
||||
binding.tvEmpty.visibility = if (adapter.itemCount == 0) View.VISIBLE else View.GONE
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(requireContext(), "목록 로드 실패: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
} finally {
|
||||
binding.progress.visibility = View.GONE
|
||||
binding.swipeRefresh.isRefreshing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteNotification(id: Long) {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
ApiClientFactory.alertApi(requireContext()).deleteNotification(id)
|
||||
}
|
||||
loadPage()
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(requireContext(), "삭제 실패: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package com.analysis.goldenanalysis.ui.alert
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.analysis.goldenanalysis.data.api.ApiClientFactory
|
||||
import com.analysis.goldenanalysis.data.model.AlertNotificationDetailDto
|
||||
import com.analysis.goldenanalysis.databinding.BottomSheetSignalDetailBinding
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SignalDetailBottomSheet : BottomSheetDialogFragment() {
|
||||
|
||||
private var _binding: BottomSheetSignalDetailBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
_binding = BottomSheetSignalDetailBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val id = arguments?.getLong(ARG_ID) ?: return dismiss()
|
||||
val symbol = arguments?.getString(ARG_SYMBOL)
|
||||
val signal = arguments?.getString(ARG_SIGNAL) ?: "BUY"
|
||||
binding.tvTitle.text = if (signal == "SELL") "📉 매도 신호 상세" else "📈 매수 신호 상세"
|
||||
binding.tvBody.text = getString(com.analysis.goldenanalysis.R.string.loading)
|
||||
binding.btnClose.setOnClickListener { dismiss() }
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
try {
|
||||
val api = ApiClientFactory.alertApi(requireContext())
|
||||
val detail = withContext(Dispatchers.IO) {
|
||||
api.getNotificationDetail(id, symbol?.trim()?.takeIf { it.isNotEmpty() })
|
||||
}
|
||||
binding.tvBody.text = buildDetailText(detail)
|
||||
} catch (e: Exception) {
|
||||
binding.tvBody.text = e.message ?: "오류"
|
||||
Toast.makeText(requireContext(), "상세를 불러오지 못했습니다.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildDetailText(d: AlertNotificationDetailDto): String {
|
||||
val sb = StringBuilder()
|
||||
d.koreanName?.let { sb.appendLine("종목: $it (${d.symbol ?: ""})") }
|
||||
d.message?.let { sb.appendLine().appendLine(it) }
|
||||
d.conditionDescription?.let { sb.appendLine().appendLine("조건: $it") }
|
||||
d.timeInterval?.let { sb.appendLine("시간봉: $it") }
|
||||
val ind = d.indicatorData
|
||||
if (ind != null) {
|
||||
sb.appendLine().appendLine("— 지표 스냅샷 —")
|
||||
ind.time?.let { sb.appendLine("시간: $it") }
|
||||
ind.price?.let { sb.appendLine("가격: $it") }
|
||||
ind.rsi?.let { sb.appendLine("RSI: $it") }
|
||||
ind.macdLine?.let { sb.appendLine("MACD: $it") }
|
||||
ind.signalLine?.let { sb.appendLine("시그널: $it") }
|
||||
ind.cci?.let { sb.appendLine("CCI: $it") }
|
||||
}
|
||||
d.snapshotClose?.let { sb.appendLine().appendLine("봉 종가(스냅): $it") }
|
||||
val conds = d.satisfiedConditions.orEmpty()
|
||||
if (conds.isNotEmpty()) {
|
||||
sb.appendLine().appendLine("— 충족 조건 —")
|
||||
conds.forEach { c ->
|
||||
sb.appendLine(
|
||||
listOfNotNull(c.indicatorType, c.conditionType, c.description)
|
||||
.joinToString(" ")
|
||||
)
|
||||
}
|
||||
}
|
||||
return sb.toString().trim()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_ID = "id"
|
||||
private const val ARG_SYMBOL = "symbol"
|
||||
private const val ARG_SIGNAL = "signal"
|
||||
|
||||
fun newInstance(notificationId: Long, rowSymbol: String?, signal: String): SignalDetailBottomSheet {
|
||||
val f = SignalDetailBottomSheet()
|
||||
f.arguments = bundleOf(
|
||||
ARG_ID to notificationId,
|
||||
ARG_SYMBOL to rowSymbol,
|
||||
ARG_SIGNAL to signal,
|
||||
)
|
||||
return f
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package com.analysis.goldenanalysis.ui.chart
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.net.http.SslError
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.SslErrorHandler
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.analysis.goldenanalysis.BuildConfig
|
||||
import com.analysis.goldenanalysis.databinding.FragmentChartBinding
|
||||
|
||||
/**
|
||||
* 차트 Fragment
|
||||
* - Frontend의 모바일 차트 페이지를 WebView로 표시
|
||||
*/
|
||||
class ChartFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentChartBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val TAG = "ChartFragment"
|
||||
private val JS_TAG = "ChartFragment-JS"
|
||||
|
||||
private fun getFrontendBaseUrl(): String {
|
||||
val prefs = requireActivity().getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||
return prefs.getString("frontend_url", "http://exdev.co.kr/mobile/chart")
|
||||
?: "http://exdev.co.kr/mobile/chart"
|
||||
}
|
||||
|
||||
/** 알림에서 저장한 심볼이 있으면 쿼리 gaSymbol 붙이고 prefs 에서 제거 (프론트 연동 시 사용). */
|
||||
private fun buildUrlConsumingPendingChartSymbol(): String {
|
||||
val prefs = requireActivity().getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||
val base = getFrontendBaseUrl()
|
||||
val sym = prefs.getString("pending_chart_symbol", null)?.trim().orEmpty()
|
||||
if (sym.isNotEmpty()) {
|
||||
prefs.edit()
|
||||
.remove("pending_chart_symbol")
|
||||
.remove("pending_chart_korean")
|
||||
.apply()
|
||||
val sep = if (base.contains("?")) "&" else "?"
|
||||
return "$base${sep}gaSymbol=${Uri.encode(sym)}"
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
Log.d(TAG, "onCreateView")
|
||||
_binding = FragmentChartBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
Log.d(TAG, "onViewCreated")
|
||||
|
||||
try {
|
||||
// Frontend 모바일 페이지를 WebView로 로드
|
||||
setupWebView()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in onViewCreated", e)
|
||||
Toast.makeText(context, "초기화 오류: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private fun setupWebView() {
|
||||
binding.webView.apply {
|
||||
webViewClient = object : WebViewClient() {
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
Log.d(TAG, "Page started loading: $url")
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
Log.d(TAG, "Page finished loading: $url")
|
||||
|
||||
// JavaScript로 페이지 내용 확인
|
||||
view?.evaluateJavascript("document.body.innerHTML.length") { result ->
|
||||
Log.d(TAG, "Page content length: $result")
|
||||
if (result == "0" || result == "null") {
|
||||
Log.e(TAG, "Page loaded but content is empty!")
|
||||
Toast.makeText(context, "페이지가 비어있습니다", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
binding.progressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onReceivedError(
|
||||
view: WebView?,
|
||||
errorCode: Int,
|
||||
description: String?,
|
||||
failingUrl: String?
|
||||
) {
|
||||
super.onReceivedError(view, errorCode, description, failingUrl)
|
||||
Log.e(TAG, "WebView Error: code=$errorCode, desc=$description, url=$failingUrl")
|
||||
Toast.makeText(context, "페이지 로딩 오류 ($errorCode): $description", Toast.LENGTH_LONG).show()
|
||||
binding.progressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in API level 23")
|
||||
override fun onReceivedError(
|
||||
view: WebView?,
|
||||
request: android.webkit.WebResourceRequest?,
|
||||
error: android.webkit.WebResourceError?
|
||||
) {
|
||||
super.onReceivedError(view, request, error)
|
||||
if (request?.isForMainFrame == true) {
|
||||
Log.e(TAG, "WebView Resource Error: ${error?.description}")
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPS 오류 무시 (개발 환경에서만 사용 권장)
|
||||
override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
|
||||
Log.w(TAG, "SSL Error: ${error?.primaryError} on ${error?.url}. Proceeding anyway.")
|
||||
handler?.proceed() // 인증서 무시
|
||||
}
|
||||
}
|
||||
|
||||
webChromeClient = object : WebChromeClient() {
|
||||
override fun onConsoleMessage(consoleMessage: android.webkit.ConsoleMessage?): Boolean {
|
||||
Log.d(JS_TAG, "${consoleMessage?.message()} -- From ${consoleMessage?.sourceId()} : ${consoleMessage?.lineNumber()}")
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onProgressChanged(view: WebView?, newProgress: Int) {
|
||||
super.onProgressChanged(view, newProgress)
|
||||
if (newProgress < 100) {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
settings.apply {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
cacheMode = WebSettings.LOAD_NO_CACHE // 캐시 비활성화 (디버깅용)
|
||||
loadWithOverviewMode = true
|
||||
useWideViewPort = true
|
||||
builtInZoomControls = false // 모바일에서는 줌 컨트롤 비활성화
|
||||
displayZoomControls = false
|
||||
setSupportZoom(true)
|
||||
allowFileAccess = true
|
||||
allowContentAccess = true
|
||||
databaseEnabled = true
|
||||
|
||||
// 네트워크 설정
|
||||
mixedContentMode = android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
||||
|
||||
// User Agent 설정 (모바일 명시)
|
||||
userAgentString = userAgentString + " GoldenAnalysisMobile/1.0"
|
||||
|
||||
// WebView 디버깅 활성화 (크롬 개발자 도구에서 확인 가능)
|
||||
if (BuildConfig.DEBUG) {
|
||||
WebView.setWebContentsDebuggingEnabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
// JavaScript 인터페이스 추가
|
||||
addJavascriptInterface(WebAppInterface(requireContext()), "Android")
|
||||
|
||||
// Frontend 모바일 페이지 로드
|
||||
val frontendUrl = buildUrlConsumingPendingChartSymbol()
|
||||
Log.d(TAG, "Loading Frontend URL: $frontendUrl")
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
|
||||
// WebView 강제 재개
|
||||
onResume()
|
||||
resumeTimers()
|
||||
|
||||
// 약간의 지연 후 로드 (WebView 초기화 대기)
|
||||
post {
|
||||
Log.d(TAG, "Starting to load URL...")
|
||||
loadUrl(frontendUrl)
|
||||
Log.d(TAG, "loadUrl() called")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
Log.d(TAG, "Fragment onResume")
|
||||
try {
|
||||
binding.webView.onResume()
|
||||
binding.webView.resumeTimers()
|
||||
// 알림 탭 등에서 pending_chart_symbol 저장 후 차트 탭으로 온 경우
|
||||
val prefs = requireActivity().getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||
if (!prefs.getString("pending_chart_symbol", null).isNullOrBlank()) {
|
||||
val url = buildUrlConsumingPendingChartSymbol()
|
||||
Log.d(TAG, "Reload chart URL: $url")
|
||||
binding.webView.loadUrl(url)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in onResume", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
Log.d(TAG, "Fragment onPause")
|
||||
try {
|
||||
binding.webView.onPause()
|
||||
binding.webView.pauseTimers()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in onPause", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
binding.webView.destroy()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
// JavaScript에서 호출할 인터페이스
|
||||
class WebAppInterface(private val mContext: Context) {
|
||||
@JavascriptInterface
|
||||
fun showToast(toast: String) {
|
||||
Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun log(message: String) {
|
||||
Log.d("ChartFragment-JS", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.analysis.goldenanalysis.ui.dashboard
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.analysis.goldenanalysis.R
|
||||
import com.analysis.goldenanalysis.databinding.FragmentDashboardBinding
|
||||
|
||||
class DashboardFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentDashboardBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
Log.d("DashboardFragment", "onCreateView")
|
||||
_binding = FragmentDashboardBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
Log.d("DashboardFragment", "onViewCreated")
|
||||
try {
|
||||
setupViews()
|
||||
} catch (e: Exception) {
|
||||
Log.e("DashboardFragment", "Error in setupViews", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
// TODO: Implement dashboard data loading from API
|
||||
try {
|
||||
binding.tvTotalAssets.text = "0원"
|
||||
binding.tvProfitLoss.text = "0.00%"
|
||||
binding.tvTodayChange.text = "0원"
|
||||
} catch (e: Exception) {
|
||||
Log.e("DashboardFragment", "Error setting text", e)
|
||||
// Views might not exist in layout
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.analysis.goldenanalysis.ui.portfolio
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.analysis.goldenanalysis.databinding.FragmentPortfolioBinding
|
||||
|
||||
class PortfolioFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentPortfolioBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentPortfolioBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupViews()
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
binding.fabAdd.setOnClickListener {
|
||||
// TODO: Open add portfolio item dialog
|
||||
}
|
||||
|
||||
// TODO: Load portfolio items from API
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package com.analysis.goldenanalysis.ui.scheduler
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.analysis.goldenanalysis.R
|
||||
import com.analysis.goldenanalysis.data.model.Schedule
|
||||
import com.analysis.goldenanalysis.databinding.DialogScheduleBinding
|
||||
|
||||
class ScheduleDialogFragment : DialogFragment() {
|
||||
|
||||
private var _binding: DialogScheduleBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var schedule: Schedule? = null
|
||||
private var onSaveListener: ((Schedule) -> Unit)? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NORMAL, R.style.Theme_GoldenAnalysis)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = DialogScheduleBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupSpinners()
|
||||
setupListeners()
|
||||
|
||||
// If editing existing schedule
|
||||
schedule?.let { populateFields(it) }
|
||||
}
|
||||
|
||||
private fun setupSpinners() {
|
||||
// Interval spinner
|
||||
val intervals = arrayOf("1분", "5분", "15분", "30분", "1시간", "4시간", "1일")
|
||||
val intervalAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, intervals)
|
||||
intervalAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
binding.spinnerInterval.adapter = intervalAdapter
|
||||
|
||||
// Notification method spinner
|
||||
val methods = arrayOf("앱 알림", "이메일", "SMS")
|
||||
val methodAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, methods)
|
||||
methodAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
binding.spinnerNotification.adapter = methodAdapter
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
binding.btnSave.setOnClickListener {
|
||||
saveSchedule()
|
||||
}
|
||||
|
||||
binding.btnCancel.setOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateFields(schedule: Schedule) {
|
||||
binding.apply {
|
||||
etScheduleName.setText(schedule.name)
|
||||
|
||||
// Set spinner selections
|
||||
val intervalPos = (spinnerInterval.adapter as ArrayAdapter<String>)
|
||||
.getPosition(schedule.interval)
|
||||
if (intervalPos >= 0) spinnerInterval.setSelection(intervalPos)
|
||||
|
||||
val methodPos = (spinnerNotification.adapter as ArrayAdapter<String>)
|
||||
.getPosition(schedule.notificationMethod)
|
||||
if (methodPos >= 0) spinnerNotification.setSelection(methodPos)
|
||||
|
||||
etIndicators.setText(schedule.indicators)
|
||||
etConditions.setText(schedule.conditions)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveSchedule() {
|
||||
val name = binding.etScheduleName.text.toString()
|
||||
if (name.isBlank()) {
|
||||
binding.etScheduleName.error = "스케쥴 이름을 입력하세요"
|
||||
return
|
||||
}
|
||||
|
||||
val newSchedule = Schedule(
|
||||
id = schedule?.id ?: 0,
|
||||
name = name,
|
||||
interval = binding.spinnerInterval.selectedItem.toString(),
|
||||
indicators = binding.etIndicators.text.toString(),
|
||||
notificationMethod = binding.spinnerNotification.selectedItem.toString(),
|
||||
conditions = binding.etConditions.text.toString(),
|
||||
isEnabled = schedule?.isEnabled ?: true,
|
||||
createdAt = schedule?.createdAt ?: System.currentTimeMillis(),
|
||||
lastExecutedAt = schedule?.lastExecutedAt ?: 0
|
||||
)
|
||||
|
||||
onSaveListener?.invoke(newSchedule)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
dialog?.window?.setLayout(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
fun setOnSaveListener(listener: (Schedule) -> Unit) {
|
||||
onSaveListener = listener
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(schedule: Schedule? = null): ScheduleDialogFragment {
|
||||
val fragment = ScheduleDialogFragment()
|
||||
fragment.schedule = schedule
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.analysis.goldenanalysis.ui.scheduler
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.analysis.goldenanalysis.data.model.Schedule
|
||||
import com.analysis.goldenanalysis.databinding.ItemScheduleBinding
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class SchedulerAdapter(
|
||||
private val onEdit: (Schedule) -> Unit,
|
||||
private val onDelete: (Schedule) -> Unit,
|
||||
private val onToggle: (Schedule, Boolean) -> Unit
|
||||
) : ListAdapter<Schedule, SchedulerAdapter.ScheduleViewHolder>(ScheduleDiffCallback()) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ScheduleViewHolder {
|
||||
val binding = ItemScheduleBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return ScheduleViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ScheduleViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
inner class ScheduleViewHolder(
|
||||
private val binding: ItemScheduleBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(schedule: Schedule) {
|
||||
binding.apply {
|
||||
tvScheduleName.text = schedule.name
|
||||
tvInterval.text = schedule.interval
|
||||
tvIndicators.text = schedule.indicators
|
||||
switchEnabled.isChecked = schedule.isEnabled
|
||||
|
||||
// Format last executed time
|
||||
if (schedule.lastExecutedAt > 0) {
|
||||
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())
|
||||
tvLastExecuted.text = "마지막 실행: ${sdf.format(Date(schedule.lastExecutedAt))}"
|
||||
} else {
|
||||
tvLastExecuted.text = "실행 기록 없음"
|
||||
}
|
||||
|
||||
// Click listeners
|
||||
root.setOnClickListener { onEdit(schedule) }
|
||||
btnEdit.setOnClickListener { onEdit(schedule) }
|
||||
btnDelete.setOnClickListener { onDelete(schedule) }
|
||||
switchEnabled.setOnCheckedChangeListener { _, isChecked ->
|
||||
onToggle(schedule, isChecked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ScheduleDiffCallback : DiffUtil.ItemCallback<Schedule>() {
|
||||
override fun areItemsTheSame(oldItem: Schedule, newItem: Schedule): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Schedule, newItem: Schedule): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package com.analysis.goldenanalysis.ui.scheduler
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.analysis.goldenanalysis.data.model.Schedule
|
||||
import com.analysis.goldenanalysis.databinding.FragmentSchedulerBinding
|
||||
|
||||
class SchedulerFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentSchedulerBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val viewModel: SchedulerViewModel by viewModels()
|
||||
private lateinit var adapter: SchedulerAdapter
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentSchedulerBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupRecyclerView()
|
||||
setupObservers()
|
||||
setupListeners()
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
adapter = SchedulerAdapter(
|
||||
onEdit = { schedule -> showEditDialog(schedule) },
|
||||
onDelete = { schedule -> viewModel.delete(schedule) },
|
||||
onToggle = { schedule, enabled ->
|
||||
viewModel.update(schedule.copy(isEnabled = enabled))
|
||||
}
|
||||
)
|
||||
|
||||
binding.rvScheduler.apply {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = this@SchedulerFragment.adapter
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupObservers() {
|
||||
viewModel.allSchedules.observe(viewLifecycleOwner) { schedules ->
|
||||
adapter.submitList(schedules)
|
||||
binding.tvEmptyState.visibility =
|
||||
if (schedules.isEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
binding.fabAdd.setOnClickListener {
|
||||
showAddDialog()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAddDialog() {
|
||||
val dialog = ScheduleDialogFragment.newInstance()
|
||||
dialog.setOnSaveListener { schedule ->
|
||||
viewModel.insert(schedule)
|
||||
}
|
||||
dialog.show(childFragmentManager, "AddScheduleDialog")
|
||||
}
|
||||
|
||||
private fun showEditDialog(schedule: Schedule) {
|
||||
val dialog = ScheduleDialogFragment.newInstance(schedule)
|
||||
dialog.setOnSaveListener { updatedSchedule ->
|
||||
viewModel.update(updatedSchedule)
|
||||
}
|
||||
dialog.show(childFragmentManager, "EditScheduleDialog")
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.analysis.goldenanalysis.ui.scheduler
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.analysis.goldenanalysis.data.database.AppDatabase
|
||||
import com.analysis.goldenanalysis.data.model.Schedule
|
||||
import com.analysis.goldenanalysis.data.repository.ScheduleRepository
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SchedulerViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private val repository: ScheduleRepository
|
||||
val allSchedules: LiveData<List<Schedule>>
|
||||
|
||||
init {
|
||||
val scheduleDao = AppDatabase.getDatabase(application).scheduleDao()
|
||||
repository = ScheduleRepository(scheduleDao)
|
||||
allSchedules = repository.allSchedules
|
||||
}
|
||||
|
||||
fun insert(schedule: Schedule) = viewModelScope.launch {
|
||||
repository.insert(schedule)
|
||||
}
|
||||
|
||||
fun update(schedule: Schedule) = viewModelScope.launch {
|
||||
repository.update(schedule)
|
||||
}
|
||||
|
||||
fun delete(schedule: Schedule) = viewModelScope.launch {
|
||||
repository.delete(schedule)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.analysis.goldenanalysis.ui.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.analysis.goldenanalysis.data.api.ApiClientFactory
|
||||
import com.analysis.goldenanalysis.databinding.FragmentSettingsBinding
|
||||
|
||||
class SettingsFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentSettingsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private lateinit var prefs: SharedPreferences
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentSettingsBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
prefs = requireContext().getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||
setupViews()
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
// Load saved settings
|
||||
binding.switchNotification.isChecked = prefs.getBoolean("notifications_enabled", true)
|
||||
binding.switchDarkMode.isChecked = prefs.getBoolean("dark_mode", false)
|
||||
// Frontend URL 설정 (차트 페이지)
|
||||
// 로컬 Docker: http://192.168.219.106/mobile/chart
|
||||
// 운영: https://exdev.co.kr/mobile/chart
|
||||
val defaultFrontendUrl = "https://exdev.co.kr/mobile/chart"
|
||||
binding.etApiServer.setText(prefs.getString("frontend_url", defaultFrontendUrl))
|
||||
binding.etApiBackend.setText(prefs.getString("api_base_url", ""))
|
||||
|
||||
// Version info
|
||||
binding.tvVersionValue.text = "1.0.0"
|
||||
|
||||
// Setup listeners
|
||||
binding.switchNotification.setOnCheckedChangeListener { _, isChecked ->
|
||||
prefs.edit().putBoolean("notifications_enabled", isChecked).apply()
|
||||
}
|
||||
|
||||
binding.switchDarkMode.setOnCheckedChangeListener { _, isChecked ->
|
||||
prefs.edit().putBoolean("dark_mode", isChecked).apply()
|
||||
applyTheme(isChecked)
|
||||
}
|
||||
|
||||
binding.btnSaveServer.setOnClickListener {
|
||||
val frontendUrl = binding.etApiServer.text.toString()
|
||||
val apiBackend = binding.etApiBackend.text.toString().trim()
|
||||
val ed = prefs.edit().putString("frontend_url", frontendUrl)
|
||||
if (apiBackend.isEmpty()) ed.remove("api_base_url") else ed.putString("api_base_url", apiBackend)
|
||||
ed.apply()
|
||||
ApiClientFactory.reset()
|
||||
android.widget.Toast.makeText(context, "저장되었습니다", android.widget.Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyTheme(isDarkMode: Boolean) {
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
if (isDarkMode) AppCompatDelegate.MODE_NIGHT_YES
|
||||
else AppCompatDelegate.MODE_NIGHT_NO
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.analysis.goldenanalysis.util
|
||||
|
||||
import com.analysis.goldenanalysis.data.model.AlertNotificationDto
|
||||
|
||||
data class StockStrategyPair(
|
||||
val koreanName: String,
|
||||
val strategyName: String,
|
||||
val symbol: String? = null,
|
||||
)
|
||||
|
||||
data class ResolvedStockRow(
|
||||
val koreanName: String,
|
||||
val strategyName: String,
|
||||
val displaySymbol: String,
|
||||
val navSymbol: String?,
|
||||
)
|
||||
|
||||
object AlertMessageParser {
|
||||
|
||||
private val marketCodeRe = Regex("^(KRW|USDT|BTC)-[A-Z0-9]+$", RegexOption.IGNORE_CASE)
|
||||
|
||||
fun extractStrategyNames(message: String): List<String>? {
|
||||
if (!message.contains("알림 조건 충족") || !message.contains("개)")) return null
|
||||
val lines = message.split("\n").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
val names = mutableListOf<String>()
|
||||
for (i in 1 until lines.size) {
|
||||
val sep = listOf(" × ", " X ").firstOrNull { lines[i].contains(it) } ?: continue
|
||||
val idx = lines[i].indexOf(sep)
|
||||
if (idx >= 0) {
|
||||
val name = lines[i].substring(idx + sep.length).trim()
|
||||
if (name.isNotEmpty()) names.add(name)
|
||||
}
|
||||
}
|
||||
return if (names.isEmpty()) null else names.distinct()
|
||||
}
|
||||
|
||||
fun extractStockStrategyPairs(message: String): List<StockStrategyPair>? {
|
||||
if (!message.contains("알림 조건 충족") || !message.contains("개)")) return null
|
||||
val lines = message.split("\n").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
val pairs = mutableListOf<StockStrategyPair>()
|
||||
for (i in 1 until lines.size) {
|
||||
val sep = listOf(" × ", " X ").firstOrNull { lines[i].contains(it) } ?: continue
|
||||
val idx = lines[i].indexOf(sep)
|
||||
if (idx >= 0) {
|
||||
val kn = lines[i].substring(0, idx).trim()
|
||||
val sn = lines[i].substring(idx + sep.length).trim()
|
||||
if (kn.isNotEmpty()) pairs.add(StockStrategyPair(kn, sn))
|
||||
}
|
||||
}
|
||||
return pairs.ifEmpty { null }
|
||||
}
|
||||
|
||||
fun resolveSymbols(koreanNames: List<String>, cryptoMap: Map<String, String>): List<Pair<String, String>> {
|
||||
val entries = cryptoMap.entries.toList()
|
||||
fun norm(s: String) = s.replace(Regex("\\s+"), " ").trim()
|
||||
val out = mutableListOf<Pair<String, String>>()
|
||||
for (kn in koreanNames) {
|
||||
val n = norm(kn)
|
||||
var matched = entries.find { norm(it.key) == n || it.key == kn }
|
||||
if (matched == null) {
|
||||
matched = entries.find {
|
||||
norm(it.key).contains(n) || n.contains(norm(it.key))
|
||||
}
|
||||
}
|
||||
if (matched != null) out.add(matched.key to matched.value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
private fun koreanNameForMarketSymbol(symbol: String, cryptoMap: Map<String, String>, fallback: String): String {
|
||||
val s = symbol.trim().uppercase()
|
||||
cryptoMap.entries.forEach { (k, v) ->
|
||||
if (v.uppercase() == s) return k
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
fun resolveStockStrategyPairRow(pair: StockStrategyPair, cryptoMap: Map<String, String>): ResolvedStockRow {
|
||||
if (!pair.symbol.isNullOrBlank()) {
|
||||
val nav = pair.symbol.trim().uppercase()
|
||||
val left = pair.koreanName.trim()
|
||||
val kn = if (left.isNotEmpty() && !marketCodeRe.matches(left)) pair.koreanName
|
||||
else koreanNameForMarketSymbol(nav, cryptoMap, left.ifEmpty { nav })
|
||||
return ResolvedStockRow(kn, pair.strategyName, nav, nav)
|
||||
}
|
||||
val raw = pair.koreanName.trim()
|
||||
if (marketCodeRe.matches(raw)) {
|
||||
val nav = raw.uppercase()
|
||||
val kn = koreanNameForMarketSymbol(nav, cryptoMap, raw)
|
||||
return ResolvedStockRow(kn, pair.strategyName, nav, nav)
|
||||
}
|
||||
val resolved = resolveSymbols(listOf(pair.koreanName), cryptoMap).firstOrNull()
|
||||
return ResolvedStockRow(
|
||||
pair.koreanName,
|
||||
pair.strategyName,
|
||||
resolved?.second ?: pair.strategyName,
|
||||
resolved?.second,
|
||||
)
|
||||
}
|
||||
|
||||
fun titleAndPairs(notification: AlertNotificationDto): Pair<String, List<StockStrategyPair>> {
|
||||
val msg = notification.message.orEmpty()
|
||||
if (msg.contains("알림 조건 충족") && msg.contains("개)")) {
|
||||
val strategyNames = extractStrategyNames(msg).orEmpty()
|
||||
val title = when {
|
||||
strategyNames.isEmpty() -> "🎯 통합 알림"
|
||||
strategyNames.size <= 3 -> strategyNames.joinToString(", ")
|
||||
else -> "${strategyNames.take(2).joinToString(", ")} 외 ${strategyNames.size - 2}개 전략"
|
||||
}
|
||||
val pairs = extractStockStrategyPairs(msg).orEmpty()
|
||||
return title to pairs
|
||||
}
|
||||
if (!notification.koreanName.isNullOrBlank()) {
|
||||
val firstLine = msg.split("\n").firstOrNull()?.trim().orEmpty()
|
||||
val strategyName = firstLine.replace(Regex("^[\\s🧪🎯✅]*"), "").trim().ifEmpty { "알림" }
|
||||
return strategyName to listOf(
|
||||
StockStrategyPair(notification.koreanName!!, strategyName, notification.symbol)
|
||||
)
|
||||
}
|
||||
return (notification.symbol ?: "알림") to emptyList()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.analysis.goldenanalysis.util
|
||||
|
||||
import com.analysis.goldenanalysis.data.model.AlertNotificationDto
|
||||
|
||||
object AlertSignalInference {
|
||||
|
||||
fun inferFromStrategyName(strategyName: String): String? {
|
||||
val s = strategyName.trim()
|
||||
if (s.isEmpty()) return null
|
||||
val hint = s.replace(Regex("\\s"), "")
|
||||
if (Regex("매도$").containsMatchIn(hint) && !Regex("과매도$").containsMatchIn(hint)) return "SELL"
|
||||
if (Regex("매수$").containsMatchIn(hint) && !Regex("과매수$").containsMatchIn(hint)) return "BUY"
|
||||
if (s.contains("데드크로스")) return "SELL"
|
||||
if (s.contains("골든크로스")) return "BUY"
|
||||
return null
|
||||
}
|
||||
|
||||
fun inferFromNotification(n: AlertNotificationDto, koreanNameForRow: String?): String? {
|
||||
val row = koreanNameForRow?.trim()
|
||||
if (!row.isNullOrEmpty()) {
|
||||
val pairs = AlertMessageParser.extractStockStrategyPairs(n.message.orEmpty()).orEmpty()
|
||||
val pair = pairs.find { it.koreanName.trim() == row }
|
||||
pair?.strategyName?.let { inferFromStrategyName(it) }?.let { return it }
|
||||
}
|
||||
val raw = "${n.message.orEmpty()}\n${n.conditionDescription.orEmpty()}"
|
||||
val pairsAll = AlertMessageParser.extractStockStrategyPairs(n.message.orEmpty()).orEmpty()
|
||||
if (pairsAll.size == 1) {
|
||||
inferFromStrategyName(pairsAll[0].strategyName)?.let { return it }
|
||||
}
|
||||
val hint = raw.replace(Regex("\\s"), "")
|
||||
if (Regex("매도$").containsMatchIn(hint) && !Regex("과매도$").containsMatchIn(hint)) return "SELL"
|
||||
if (Regex("매수$").containsMatchIn(hint) && !Regex("과매수$").containsMatchIn(hint)) return "BUY"
|
||||
if (raw.contains("데드크로스")) return "SELL"
|
||||
if (raw.contains("골든크로스")) return "BUY"
|
||||
val masked = raw.replace("과매도", "").replace("과매수", "")
|
||||
if (Regex("매도조건|매도신호").containsMatchIn(masked) &&
|
||||
!Regex("매수조건|매수신호").containsMatchIn(masked)
|
||||
) return "SELL"
|
||||
if (Regex("매수조건|매수신호").containsMatchIn(masked) &&
|
||||
!Regex("매도조건|매도신호").containsMatchIn(masked)
|
||||
) return "BUY"
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.analysis.goldenanalysis.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
|
||||
object ApiPrefs {
|
||||
private const val PREFS = "app_prefs"
|
||||
private const val KEY_API_BASE = "api_base_url"
|
||||
private const val KEY_FRONTEND = "frontend_url"
|
||||
|
||||
fun prefs(context: Context): SharedPreferences =
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
|
||||
/**
|
||||
* 백엔드 REST 베이스 (…/api/ 형태, Retrofit용).
|
||||
* [api_base_url]이 있으면 사용, 없으면 [frontend_url]의 origin + /api/
|
||||
*/
|
||||
fun getApiBaseUrl(context: Context): String {
|
||||
val p = prefs(context)
|
||||
val explicit = p.getString(KEY_API_BASE, null)?.trim().orEmpty()
|
||||
if (explicit.isNotEmpty()) {
|
||||
var b = explicit.trimEnd('/')
|
||||
if (!b.endsWith("/api")) b = "$b/api"
|
||||
return "$b/"
|
||||
}
|
||||
val front = p.getString(KEY_FRONTEND, "https://exdev.co.kr/mobile/chart")
|
||||
?: "https://exdev.co.kr/mobile/chart"
|
||||
val uri = Uri.parse(front)
|
||||
val scheme = uri.scheme ?: "https"
|
||||
val host = uri.host ?: "exdev.co.kr"
|
||||
val port = if (uri.port == -1 || uri.port == 80 || uri.port == 443) "" else ":${uri.port}"
|
||||
return "$scheme://$host$port/api/"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.analysis.goldenanalysis.util
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.*
|
||||
import com.analysis.goldenanalysis.data.model.Schedule
|
||||
import com.analysis.goldenanalysis.worker.ScheduleWorker
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object ScheduleManager {
|
||||
|
||||
fun scheduleWork(context: Context, schedule: Schedule) {
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
|
||||
if (!schedule.isEnabled) {
|
||||
cancelWork(context, schedule.id)
|
||||
return
|
||||
}
|
||||
|
||||
val intervalMinutes = getIntervalInMinutes(schedule.interval)
|
||||
val inputData = Data.Builder()
|
||||
.putLong("schedule_id", schedule.id)
|
||||
.build()
|
||||
|
||||
val workRequest = PeriodicWorkRequestBuilder<ScheduleWorker>(
|
||||
intervalMinutes,
|
||||
TimeUnit.MINUTES
|
||||
)
|
||||
.setInputData(inputData)
|
||||
.setConstraints(
|
||||
Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
)
|
||||
.addTag("schedule_${schedule.id}")
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniquePeriodicWork(
|
||||
"schedule_${schedule.id}",
|
||||
ExistingPeriodicWorkPolicy.REPLACE,
|
||||
workRequest
|
||||
)
|
||||
}
|
||||
|
||||
fun cancelWork(context: Context, scheduleId: Long) {
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
workManager.cancelUniqueWork("schedule_$scheduleId")
|
||||
}
|
||||
|
||||
private fun getIntervalInMinutes(interval: String): Long {
|
||||
return when (interval) {
|
||||
"1분" -> 15 // Minimum 15 minutes for WorkManager
|
||||
"5분" -> 15
|
||||
"15분" -> 15
|
||||
"30분" -> 30
|
||||
"1시간" -> 60
|
||||
"4시간" -> 240
|
||||
"1일" -> 1440
|
||||
else -> 60
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.analysis.goldenanalysis.util
|
||||
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
object UpbitWebUrls {
|
||||
|
||||
fun mapAlertTimeIntervalToResolution(timeInterval: String?): String? {
|
||||
if (timeInterval.isNullOrBlank()) return null
|
||||
val raw = timeInterval.trim()
|
||||
if (Regex("^\\d+M$").matches(raw)) {
|
||||
return mapOf("1M" to "1M", "3M" to "3M", "6M" to "6M", "12M" to "12M")[raw]
|
||||
}
|
||||
val key = raw.lowercase()
|
||||
val map = mapOf(
|
||||
"1m" to "1", "3m" to "3", "5m" to "5", "10m" to "10",
|
||||
"15m" to "15", "30m" to "30", "60m" to "60", "1h" to "60",
|
||||
"4h" to "240", "240m" to "240", "1d" to "1D", "1w" to "1W",
|
||||
)
|
||||
return map[key]
|
||||
}
|
||||
|
||||
fun getUpbitWebExchangeUrl(market: String?, timeInterval: String?): String? {
|
||||
if (market.isNullOrBlank()) return null
|
||||
var m = market.trim().uppercase()
|
||||
if (m.startsWith("CRIX.UPBIT.")) m = m.removePrefix("CRIX.UPBIT.")
|
||||
if (!Regex("^(KRW|USDT|BTC)-[A-Z0-9]+$", RegexOption.IGNORE_CASE).matches(m)) return null
|
||||
val code = "CRIX.UPBIT.$m"
|
||||
val base = "https://www.upbit.com/exchange?code=" + URLEncoder.encode(code, StandardCharsets.UTF_8.name())
|
||||
val res = mapAlertTimeIntervalToResolution(timeInterval)
|
||||
return if (res != null) "$base&resolution=" + URLEncoder.encode(res, StandardCharsets.UTF_8.name()) else base
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.analysis.goldenanalysis.worker
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.analysis.goldenanalysis.R
|
||||
import com.analysis.goldenanalysis.data.database.AppDatabase
|
||||
import com.analysis.goldenanalysis.data.repository.ScheduleRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ScheduleWorker(
|
||||
context: Context,
|
||||
params: WorkerParameters
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
private val repository: ScheduleRepository by lazy {
|
||||
val dao = AppDatabase.getDatabase(context).scheduleDao()
|
||||
ScheduleRepository(dao)
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val scheduleId = inputData.getLong("schedule_id", -1)
|
||||
if (scheduleId == -1L) return@withContext Result.failure()
|
||||
|
||||
val schedule = repository.getScheduleById(scheduleId)
|
||||
if (schedule == null || !schedule.isEnabled) {
|
||||
return@withContext Result.failure()
|
||||
}
|
||||
|
||||
// TODO: Call API to check indicators
|
||||
// For now, just show notification
|
||||
showNotification(schedule.name, "스케쥴이 실행되었습니다")
|
||||
|
||||
// Update last executed timestamp
|
||||
repository.updateLastExecutedAt(scheduleId, System.currentTimeMillis())
|
||||
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showNotification(title: String, message: String) {
|
||||
val notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE)
|
||||
as NotificationManager
|
||||
|
||||
val channelId = "schedule_notifications"
|
||||
|
||||
// Create notification channel for Android O and above
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
channelId,
|
||||
"스케쥴 알림",
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
)
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
val notification = NotificationCompat.Builder(applicationContext, channelId)
|
||||
.setSmallIcon(R.drawable.ic_logo)
|
||||
.setContentTitle(title)
|
||||
.setContentText(message)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
|
||||
notificationManager.notify(System.currentTimeMillis().toInt(), notification)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M19,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5C21,3.9 20.1,3 19,3zM9,17H7v-7h2V17zM13,17h-2V7h2V17zM17,17h-2v-4h2V17z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/darker_gray"
|
||||
android:pathData="M3.5,18.49l6,-6.01 4,4L22,6.92l-1.41,-1.41 -7.09,7.97 -4,-4L2,16.99z"/>
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/darker_gray"
|
||||
android:pathData="M3,13h8L11,3L3,3v10zM3,21h8v-6L3,15v6zM13,21h8L21,11h-8v10zM13,3v6h8L21,3h-8z"/>
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M3,14h4v-4H3V14zM3,19h4v-4H3V19zM3,9h4V5H3V9zM8,14h13v-4H8V14zM8,19h13v-4H8V19zM8,5v4h13V5H8z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<group android:scaleX="0.5"
|
||||
android:scaleY="0.5"
|
||||
android:translateX="27"
|
||||
android:translateY="27">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M12,2L2,7v10c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12L20,7l-10,-5zM12,12c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM18,17h-12v-0.5c0,-2 4,-3.5 6,-3.5s6,1.5 6,3.5L18,17z"/>
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="120dp"
|
||||
android:height="120dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/white"
|
||||
android:pathData="M12,2L2,7v10c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12L20,7l-10,-5zM12,12c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM18,17h-12v-0.5c0,-2 4,-3.5 6,-3.5s6,1.5 6,3.5L18,17z"/>
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32V4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#8BC34A"
|
||||
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32V4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M19,19H5V5h7V3H5c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2v7zM14,3v2h3.59l-9.83,9.83 1.41,1.41L19,6.41V10h2V3h-7z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/darker_gray"
|
||||
android:pathData="M20,6h-2.18c0.11,-0.31 0.18,-0.65 0.18,-1 0,-1.66 -1.34,-3 -3,-3 -1.05,0 -1.96,0.54 -2.5,1.35l-0.5,0.67 -0.5,-0.68C10.96,2.54 10.05,2 9,2 7.34,2 6,3.34 6,5c0,0.35 0.07,0.69 0.18,1L4,6c-1.11,0 -1.99,0.89 -1.99,2L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM15,4c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM9,4c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM20,19L4,19L4,8h5.08L7,10.83 8.62,12 11,8.76l1,-1.36 1,1.36L15.38,12 17,10.83 14.92,8L20,8v11z"/>
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/darker_gray"
|
||||
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/darker_gray"
|
||||
android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94 0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6 3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/primary"/>
|
||||
<item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@drawable/ic_logo"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/nav_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/windowBackground"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:menu="@menu/bottom_nav_menu" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_host_fragment_activity_main"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:defaultNavHost="true"
|
||||
app:layout_constraintBottom_toTopOf="@id/nav_view"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:navGraph="@navigation/mobile_navigation" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/primary">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivLogo"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="120dp"
|
||||
android:src="@drawable/ic_logo"
|
||||
android:contentDescription="@string/app_name"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintVertical_bias="0.4"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAppName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/white"
|
||||
android:layout_marginTop="24dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/ivLogo"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="투자 분석의 새로운 기준"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/white"
|
||||
android:alpha="0.8"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvAppName"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminateTint="@color/white"
|
||||
android:layout_marginBottom="48dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="📈 매수 신호 상세" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:maxHeight="400dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvBody"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textIsSelectable="true"
|
||||
android:textSize="13sp"
|
||||
tools:text="내용" />
|
||||
</ScrollView>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnClose"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/ok" />
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,122 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="스케쥴 추가/수정"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text_primary"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/scheduler_name"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etScheduleName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/scheduler_interval"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinnerInterval"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"/>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/scheduler_indicators"
|
||||
android:layout_marginTop="16dp"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etIndicators"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:hint="예: MACD, RSI, Stochastic"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/scheduler_notification"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinnerNotification"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"/>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/scheduler_condition"
|
||||
android:layout_marginTop="16dp"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etConditions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textMultiLine"
|
||||
android:minLines="2"
|
||||
android:hint="예: MACD > 0, RSI < 30"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="24dp"
|
||||
android:gravity="end">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnCancel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/scheduler_cancel"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSave"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/scheduler_save"
|
||||
android:layout_marginStart="8dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/background_dark">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="@color/alert_popup_header"
|
||||
app:title="@string/nav_alerts"
|
||||
app:titleTextColor="@color/alert_popup_text_primary" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
android:padding="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/alerts_unread_section"
|
||||
android:textColor="@color/alert_popup_text_secondary"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/cardsContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvEmpty"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:padding="24dp"
|
||||
android:text="@string/alerts_empty"
|
||||
android:textColor="@color/alert_popup_text_secondary"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/background"
|
||||
tools:context=".ui.chart.ChartFragment">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminateTint="@color/primary"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,153 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- Total Assets Card -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/cardTotalAssets"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="4dp"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/dashboard_total_assets"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/text_secondary"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTotalAssets"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="0원"
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text_primary"
|
||||
android:layout_marginTop="8dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Profit/Loss and Today Change Row -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutStats"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="16dp"
|
||||
android:weightSum="2"
|
||||
app:layout_constraintTop_toBottomOf="@id/cardTotalAssets">
|
||||
|
||||
<!-- Profit/Loss Card -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/dashboard_profit_loss"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/text_secondary"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvProfitLoss"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="0.00%"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/success"
|
||||
android:layout_marginTop="4dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Today Change Card -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/dashboard_today_change"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/text_secondary"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTodayChange"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="0원"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/chart_up"
|
||||
android:layout_marginTop="4dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Recent Activity Section -->
|
||||
<TextView
|
||||
android:id="@+id/tvRecentActivity"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="최근 활동"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text_primary"
|
||||
android:layout_marginTop="24dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/layoutStats"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvRecentActivity"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvRecentActivity"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/background_dark">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="@color/alert_popup_header"
|
||||
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" />
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:padding="8dp"
|
||||
tools:listitem="@layout/item_notification_history" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvEmpty"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:padding="24dp"
|
||||
android:text="@string/alerts_empty"
|
||||
android:textColor="@color/alert_popup_text_secondary"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/portfolio_title"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text_primary"/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvPortfolio"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvEmptyState"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/portfolio_no_items"
|
||||
android:textSize="16sp"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="48dp"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fabAdd"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:contentDescription="@string/portfolio_add"
|
||||
android:src="@android:drawable/ic_input_add"
|
||||
app:tint="@color/white"/>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/scheduler_title"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text_primary"/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvScheduler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvEmptyState"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/scheduler_no_items"
|
||||
android:textSize="16sp"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="48dp"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fabAdd"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:contentDescription="@string/scheduler_add"
|
||||
android:src="@android:drawable/ic_input_add"
|
||||
app:tint="@color/white"/>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_title"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text_primary"
|
||||
android:layout_marginBottom="24dp"/>
|
||||
|
||||
<!-- Notifications Section -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="2dp"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_notification"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text_primary"
|
||||
android:layout_marginBottom="12dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="알림 활성화"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/text_secondary"/>
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/switchNotification"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Theme Section -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="2dp"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_theme"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text_primary"
|
||||
android:layout_marginBottom="12dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="다크 모드"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/text_secondary"/>
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/switchDarkMode"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- API Server Section -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="2dp"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_api_server"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text_primary"
|
||||
android:layout_marginBottom="12dp"/>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="Frontend (모바일 차트) URL"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etApiServer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri"
|
||||
android:maxLines="1"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="백엔드 API (선택, 예: http://IP:8083/api/)"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etApiBackend"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri"
|
||||
android:maxLines="1" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSaveServer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/save"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="8dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Version Info Section -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="2dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_version"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text_primary"
|
||||
android:layout_marginBottom="12dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="앱 버전"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/text_secondary"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvVersionValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="1.0.0"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/text_primary"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
app:cardBackgroundColor="@color/alert_popup_bg"
|
||||
app:strokeColor="@color/alert_popup_border"
|
||||
app:strokeWidth="1dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="6dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/headerBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingVertical="10dp"
|
||||
android:background="@color/alert_popup_header">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:src="@drawable/ic_notifications_filled"
|
||||
android:contentDescription="@null" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCardTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
android:textColor="@color/alert_popup_text_primary"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"
|
||||
tools:text="이동평균선5일 1, 알람 CCI 외 2개 전략" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnInfo"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/alert_detail_info"
|
||||
android:src="@android:drawable/ic_menu_info_details"
|
||||
android:tint="@color/alert_popup_text_primary" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnClose"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/alert_close"
|
||||
android:src="@android:drawable/ic_menu_close_clear_cancel"
|
||||
android:tint="@color/alert_popup_text_primary" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="#3D4558" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/scrollRows"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxHeight="220dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/containerRows"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" />
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSingleLineMessage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="12dp"
|
||||
android:textColor="@color/alert_popup_text_secondary"
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone"
|
||||
tools:text="메시지" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:gravity="center_vertical"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvKoreanName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/alert_popup_text_primary"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="게임빌드" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSymbol"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/alert_popup_text_secondary"
|
||||
android:textSize="11sp"
|
||||
tools:text="KRW-GAME2" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnAlertList"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/alert_action_list"
|
||||
android:src="@drawable/ic_format_list"
|
||||
android:tint="@color/alert_popup_text_primary" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnUpbit"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/alert_action_upbit"
|
||||
android:src="@drawable/ic_open_in_new"
|
||||
android:tint="@color/success" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnSignalDetail"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/alert_action_signal_detail"
|
||||
android:src="@drawable/ic_analytics"
|
||||
android:tint="@color/primary_light" />
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:cardBackgroundColor="@color/alert_popup_bg"
|
||||
app:strokeColor="#3D4558"
|
||||
app:strokeWidth="1dp"
|
||||
app:cardCornerRadius="6dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/alert_popup_text_primary"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="전략명" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTime"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/alert_popup_text_secondary"
|
||||
android:textSize="11sp"
|
||||
tools:text="2025-01-01" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/rowActions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="end"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnDelete"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/delete"
|
||||
android:src="@android:drawable/ic_menu_delete"
|
||||
android:tint="@color/alert_popup_text_secondary" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/containerRows"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="8dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="2dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvScheduleName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="스케쥴 이름"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text_primary"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/switchEnabled"/>
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/switchEnabled"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvInterval"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="간격: 1시간"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvScheduleName"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvIndicators"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="MACD, RSI"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:layout_marginTop="4dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvInterval"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLastExecuted"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="마지막 실행: 2024-01-25 10:00"
|
||||
android:textSize="11sp"
|
||||
android:textColor="@color/text_hint"
|
||||
android:layout_marginTop="4dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvIndicators"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="12dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvLastExecuted"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnEdit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/scheduler_edit"
|
||||
android:textSize="12sp"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnDelete"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/scheduler_delete"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/error"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/navigation_dashboard"
|
||||
android:icon="@drawable/ic_dashboard"
|
||||
android:title="@string/nav_dashboard" />
|
||||
|
||||
<item
|
||||
android:id="@+id/navigation_portfolio"
|
||||
android:icon="@drawable/ic_portfolio"
|
||||
android:title="@string/nav_portfolio" />
|
||||
|
||||
<item
|
||||
android:id="@+id/navigation_chart"
|
||||
android:icon="@drawable/ic_chart"
|
||||
android:title="@string/nav_chart" />
|
||||
|
||||
<item
|
||||
android:id="@+id/navigation_alerts"
|
||||
android:icon="@drawable/ic_nav_notifications"
|
||||
android:title="@string/nav_alerts" />
|
||||
|
||||
<item
|
||||
android:id="@+id/navigation_scheduler"
|
||||
android:icon="@drawable/ic_scheduler"
|
||||
android:title="@string/nav_scheduler" />
|
||||
|
||||
<item
|
||||
android:id="@+id/navigation_settings"
|
||||
android:icon="@drawable/ic_settings"
|
||||
android:title="@string/nav_settings" />
|
||||
</menu>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_all_notifications"
|
||||
android:title="@string/alerts_menu_all"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/primary"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/primary"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/mobile_navigation"
|
||||
app:startDestination="@+id/navigation_dashboard">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_dashboard"
|
||||
android:name="com.analysis.goldenanalysis.ui.dashboard.DashboardFragment"
|
||||
android:label="@string/nav_dashboard"
|
||||
tools:layout="@layout/fragment_dashboard" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_portfolio"
|
||||
android:name="com.analysis.goldenanalysis.ui.portfolio.PortfolioFragment"
|
||||
android:label="@string/nav_portfolio"
|
||||
tools:layout="@layout/fragment_portfolio" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_chart"
|
||||
android:name="com.analysis.goldenanalysis.ui.chart.ChartFragment"
|
||||
android:label="@string/nav_chart"
|
||||
tools:layout="@layout/fragment_chart" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_alerts"
|
||||
android:name="com.analysis.goldenanalysis.ui.alert.AlertsFragment"
|
||||
android:label="@string/nav_alerts"
|
||||
tools:layout="@layout/fragment_alerts">
|
||||
<action
|
||||
android:id="@+id/action_alerts_to_notification_list"
|
||||
app:destination="@id/navigation_notification_list" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_notification_list"
|
||||
android:name="com.analysis.goldenanalysis.ui.alert.NotificationListFragment"
|
||||
android:label="@string/alerts_menu_all"
|
||||
tools:layout="@layout/fragment_notification_list">
|
||||
<argument
|
||||
android:name="filterSymbol"
|
||||
app:argType="string"
|
||||
android:defaultValue="" />
|
||||
<argument
|
||||
android:name="filterKorean"
|
||||
app:argType="string"
|
||||
android:defaultValue="" />
|
||||
<argument
|
||||
android:name="highlightId"
|
||||
app:argType="long"
|
||||
android:defaultValue="0L" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_scheduler"
|
||||
android:name="com.analysis.goldenanalysis.ui.scheduler.SchedulerFragment"
|
||||
android:label="@string/nav_scheduler"
|
||||
tools:layout="@layout/fragment_scheduler" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_settings"
|
||||
android:name="com.analysis.goldenanalysis.ui.settings.SettingsFragment"
|
||||
android:label="@string/nav_settings"
|
||||
tools:layout="@layout/fragment_settings" />
|
||||
|
||||
</navigation>
|
||||
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
|
||||
<!-- App Colors -->
|
||||
<color name="primary">#1976D2</color>
|
||||
<color name="primary_dark">#115293</color>
|
||||
<color name="primary_light">#63A4FF</color>
|
||||
<color name="accent">#FF9800</color>
|
||||
<color name="accent_dark">#F57C00</color>
|
||||
<color name="accent_light">#FFB74D</color>
|
||||
|
||||
<!-- Text Colors -->
|
||||
<color name="text_primary">#212121</color>
|
||||
<color name="text_secondary">#757575</color>
|
||||
<color name="text_hint">#BDBDBD</color>
|
||||
|
||||
<!-- Background Colors -->
|
||||
<color name="background">#FAFAFA</color>
|
||||
<color name="background_dark">#303030</color>
|
||||
<color name="card_background">#FFFFFF</color>
|
||||
|
||||
<!-- Status Colors -->
|
||||
<color name="success">#4CAF50</color>
|
||||
<color name="warning">#FFC107</color>
|
||||
<color name="error">#F44336</color>
|
||||
<color name="info">#2196F3</color>
|
||||
|
||||
<!-- Chart Colors -->
|
||||
<color name="chart_up">#E53935</color>
|
||||
<color name="chart_down">#1E88E5</color>
|
||||
|
||||
<!-- 알림 팝업(프론트 유사 다크 톤) -->
|
||||
<color name="alert_popup_bg">#1E2433</color>
|
||||
<color name="alert_popup_header">#252B3D</color>
|
||||
<color name="alert_popup_border">#829EF1</color>
|
||||
<color name="alert_popup_text_primary">#E8EAF0</color>
|
||||
<color name="alert_popup_text_secondary">#9AA3B5</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Golden Analysis</string>
|
||||
|
||||
<!-- Navigation -->
|
||||
<string name="nav_dashboard">대시보드</string>
|
||||
<string name="nav_portfolio">투자종목</string>
|
||||
<string name="nav_chart">차트</string>
|
||||
<string name="nav_alerts">알림</string>
|
||||
<string name="nav_scheduler">스케쥴러</string>
|
||||
<string name="nav_settings">설정</string>
|
||||
|
||||
<!-- Dashboard -->
|
||||
<string name="dashboard_title">투자 대시보드</string>
|
||||
<string name="dashboard_total_assets">총 자산</string>
|
||||
<string name="dashboard_profit_loss">수익률</string>
|
||||
<string name="dashboard_today_change">오늘 변동</string>
|
||||
|
||||
<!-- Portfolio -->
|
||||
<string name="portfolio_title">투자 종목</string>
|
||||
<string name="portfolio_no_items">투자 종목이 없습니다</string>
|
||||
<string name="portfolio_add">종목 추가</string>
|
||||
|
||||
<!-- Chart -->
|
||||
<string name="chart_title">차트 분석</string>
|
||||
<string name="chart_loading">차트를 불러오는 중...</string>
|
||||
|
||||
<!-- Scheduler -->
|
||||
<string name="scheduler_title">스케쥴러</string>
|
||||
<string name="scheduler_no_items">등록된 스케쥴이 없습니다</string>
|
||||
<string name="scheduler_add">스케쥴 추가</string>
|
||||
<string name="scheduler_name">스케쥴 이름</string>
|
||||
<string name="scheduler_interval">스케쥴 간격</string>
|
||||
<string name="scheduler_indicators">보조지표</string>
|
||||
<string name="scheduler_notification">알림 방법</string>
|
||||
<string name="scheduler_condition">조건</string>
|
||||
<string name="scheduler_enabled">활성화</string>
|
||||
<string name="scheduler_edit">수정</string>
|
||||
<string name="scheduler_delete">삭제</string>
|
||||
<string name="scheduler_save">저장</string>
|
||||
<string name="scheduler_cancel">취소</string>
|
||||
|
||||
<!-- Settings -->
|
||||
<string name="settings_title">설정</string>
|
||||
<string name="settings_notification">알림 설정</string>
|
||||
<string name="settings_theme">테마</string>
|
||||
<string name="settings_api_server">API 서버</string>
|
||||
<string name="settings_version">버전 정보</string>
|
||||
<string name="settings_about">앱 정보</string>
|
||||
|
||||
<!-- Common -->
|
||||
<string name="ok">확인</string>
|
||||
<string name="cancel">취소</string>
|
||||
<string name="save">저장</string>
|
||||
<string name="delete">삭제</string>
|
||||
<string name="edit">수정</string>
|
||||
<string name="search">검색</string>
|
||||
<string name="loading">로딩 중...</string>
|
||||
<string name="error">오류가 발생했습니다</string>
|
||||
<string name="retry">재시도</string>
|
||||
|
||||
<!-- Alerts -->
|
||||
<string name="alerts_unread_section">새 알림 (미읽음)</string>
|
||||
<string name="alerts_empty">표시할 알림이 없습니다.</string>
|
||||
<string name="alerts_menu_all">전체 알림</string>
|
||||
<string name="alert_close">닫기</string>
|
||||
<string name="alert_detail_info">상세 정보</string>
|
||||
<string name="alert_action_list">알림목록으로 이동</string>
|
||||
<string name="alert_action_upbit">업비트에서 차트 열기</string>
|
||||
<string name="alert_action_signal_detail">매수·매도 신호 상세</string>
|
||||
</resources>
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Base application theme -->
|
||||
<style name="Theme.GoldenAnalysis" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color -->
|
||||
<item name="colorPrimary">@color/primary</item>
|
||||
<item name="colorPrimaryVariant">@color/primary_dark</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color -->
|
||||
<item name="colorSecondary">@color/accent</item>
|
||||
<item name="colorSecondaryVariant">@color/accent_dark</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color -->
|
||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here -->
|
||||
<item name="android:windowBackground">@color/background</item>
|
||||
</style>
|
||||
|
||||
<!-- Splash theme -->
|
||||
<style name="Theme.GoldenAnalysis.Splash" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<item name="android:windowBackground">@color/primary</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
</full-backup-content>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
</cloud-backup>
|
||||
</data-extraction-rules>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<!-- 모든 도메인에 대해 cleartext traffic 허용 (개발용) -->
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<trust-anchors>
|
||||
<!-- 시스템 인증서 신뢰 -->
|
||||
<certificates src="system" />
|
||||
<!-- 사용자 인증서 신뢰 -->
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
|
||||
<!-- exdev.co.kr 도메인 특별 설정 -->
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">exdev.co.kr</domain>
|
||||
<domain includeSubdomains="true">aidev.iptime.org</domain>
|
||||
<domain includeSubdomains="true">10.0.2.2</domain>
|
||||
<domain includeSubdomains="true">192.168.0.0</domain>
|
||||
<domain includeSubdomains="true">192.168.1.0</domain>
|
||||
<domain includeSubdomains="true">localhost</domain>
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
Reference in New Issue
Block a user