From 3a5817625fffe002ef175dd898c2acc22c39dfb7 Mon Sep 17 00:00:00 2001 From: Ahn Kiwook Date: Wed, 18 Feb 2026 13:55:22 +0900 Subject: [PATCH 1/5] =?UTF-8?q?.idea=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/AndroidProjectSystem.xml | 6 ++++++ .idea/deploymentTargetSelector.xml | 10 ++++++++++ .idea/misc.xml | 5 ++++- .idea/runConfigurations.xml | 17 +++++++++++++++++ .idea/vcs.xml | 2 +- 5 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 .idea/AndroidProjectSystem.xml create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 .idea/runConfigurations.xml diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b2c751a..7c2d85a 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,9 @@ - + + + + diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 94a25f7..35eb1dd 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file From 064a83a68bdb88e7e9d3b505b557886a5145efde Mon Sep 17 00:00:00 2001 From: Ahn Kiwook Date: Wed, 18 Feb 2026 14:15:47 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=EB=A6=B0=ED=8A=B8=20=EC=A0=9C=EC=95=88=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/kotlin/pe/aioo/openmoa/OpenMoaIME.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/pe/aioo/openmoa/OpenMoaIME.kt b/app/src/main/kotlin/pe/aioo/openmoa/OpenMoaIME.kt index 240803a..abeca59 100644 --- a/app/src/main/kotlin/pe/aioo/openmoa/OpenMoaIME.kt +++ b/app/src/main/kotlin/pe/aioo/openmoa/OpenMoaIME.kt @@ -29,6 +29,7 @@ import androidx.autofill.inline.common.TextViewStyle import androidx.autofill.inline.common.ViewStyle import androidx.autofill.inline.v1.InlineSuggestionUi import androidx.core.content.ContextCompat +import androidx.core.view.isEmpty import androidx.localbroadcastmanager.content.LocalBroadcastManager import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -64,7 +65,7 @@ class OpenMoaIME : InputMethodService(), KoinComponent { @Suppress("DEPRECATION") intent.getSerializableExtra(EXTRA_NAME) } - return if (extra is T) extra else null + return extra as? T } private fun sendKeyDownUpEvent(keyCode: Int, metaState: Int = 0, withShift: Boolean = false) { @@ -581,13 +582,13 @@ class OpenMoaIME : InputMethodService(), KoinComponent { binding.suggestionStripEndChipGroup.removeAllViews() binding.suggestionStripLayout.visibility = if (response.inlineSuggestions.isEmpty()) View.GONE else View.VISIBLE - response.inlineSuggestions.map { inlineSuggestion -> + response.inlineSuggestions.forEach { inlineSuggestion -> val size = Size( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT ) inlineSuggestion.inflate(this, size, mainExecutor) { view -> if (inlineSuggestion.info.isPinned) { - if (binding.suggestionStripStartChipGroup.childCount == 0) { + if (binding.suggestionStripStartChipGroup.isEmpty()) { binding.suggestionStripStartChipGroup.addView(view) } else { binding.suggestionStripEndChipGroup.addView(view) From 1886695c33e51ce21cd02ef66cb70b27d0a81941 Mon Sep 17 00:00:00 2001 From: Ahn Kiwook Date: Wed, 18 Feb 2026 15:26:39 +0900 Subject: [PATCH 3/5] =?UTF-8?q?EmojiView=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 3 +- .../openmoa/view/keyboardview/EmojiView.kt | 60 +++++++++++++++++ app/src/main/res/layout/emoji_view.xml | 64 +++++++++++++++++++ app/src/main/res/values-night/colors.xml | 1 + app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/styles.xml | 9 +++ build.gradle | 4 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 9 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 app/src/main/kotlin/pe/aioo/openmoa/view/keyboardview/EmojiView.kt create mode 100644 app/src/main/res/layout/emoji_view.xml diff --git a/app/build.gradle b/app/build.gradle index 3f9ccd1..161057c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,7 +5,7 @@ plugins { android { namespace 'pe.aioo.openmoa' - compileSdk 33 + compileSdk 34 defaultConfig { applicationId "pe.aioo.openmoa" @@ -41,6 +41,7 @@ dependencies { implementation "androidx.autofill:autofill:1.1.0" implementation 'com.google.android.material:material:1.7.0' implementation 'io.insert-koin:koin-android:3.3.0' + implementation 'androidx.emoji2:emoji2-emojipicker:1.4.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.4' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' diff --git a/app/src/main/kotlin/pe/aioo/openmoa/view/keyboardview/EmojiView.kt b/app/src/main/kotlin/pe/aioo/openmoa/view/keyboardview/EmojiView.kt new file mode 100644 index 0000000..d75f74b --- /dev/null +++ b/app/src/main/kotlin/pe/aioo/openmoa/view/keyboardview/EmojiView.kt @@ -0,0 +1,60 @@ +package pe.aioo.openmoa.view.keyboardview + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.util.AttributeSet +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import pe.aioo.openmoa.OpenMoaIME +import pe.aioo.openmoa.R +import pe.aioo.openmoa.databinding.EmojiViewBinding +import pe.aioo.openmoa.view.keytouchlistener.RepeatKeyTouchListener +import pe.aioo.openmoa.view.keytouchlistener.SimpleKeyTouchListener +import pe.aioo.openmoa.view.message.SpecialKey +import pe.aioo.openmoa.view.message.SpecialKeyMessage + +class EmojiView : ConstraintLayout { + + constructor(context: Context) : super(context) { + init() + } + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + init() + } + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super( + context, + attrs, + defStyle, + ) { + init() + } + + private lateinit var binding: EmojiViewBinding + + @SuppressLint("ClickableViewAccessibility") + private fun init() { + inflate(context, R.layout.emoji_view, this) + binding = EmojiViewBinding.bind(this) + + val broadcastManager = LocalBroadcastManager.getInstance(context) + binding.emojiPickerView.setOnEmojiPickedListener { item -> + broadcastManager.sendBroadcast( + Intent(OpenMoaIME.INTENT_ACTION).apply { + putExtra(OpenMoaIME.EXTRA_NAME, item.emoji) + } + ) + } + + binding.closeButton.setOnTouchListener( + SimpleKeyTouchListener(context, SpecialKeyMessage(SpecialKey.EMOJI)) + ) + binding.backspaceKey.setOnTouchListener( + RepeatKeyTouchListener(context, SpecialKeyMessage(SpecialKey.BACKSPACE)) + ) + binding.enterKey.setOnTouchListener( + SimpleKeyTouchListener(context, SpecialKeyMessage(SpecialKey.ENTER)) + ) + } + +} diff --git a/app/src/main/res/layout/emoji_view.xml b/app/src/main/res/layout/emoji_view.xml new file mode 100644 index 0000000..43b16c5 --- /dev/null +++ b/app/src/main/res/layout/emoji_view.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 3cc9f54..c9d6dd9 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -5,6 +5,7 @@ #333333 #222222 #FFFFFF + #BBBBBB #5E97EE \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index ab5d630..bc0c98b 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -5,6 +5,7 @@ #FFFFFF #EEEEEE #000000 + #444444 #5E97EE \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1ad000a..8ceb60c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -21,6 +21,7 @@ 8 ใ…ก ๐Ÿ˜€ + โŒจ ๋์œผ๋กœ โŽ 5 diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index e84d805..c90e233 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -18,4 +18,13 @@ @color/keyboard_background + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index e41be9a..eb3d910 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.8.1' apply false - id 'com.android.library' version '8.8.1' apply false + id 'com.android.application' version '8.12.0' apply false + id 'com.android.library' version '8.12.0' apply false id 'org.jetbrains.kotlin.android' version '1.7.20' apply false } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df97d72..37f853b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 2e2362a869aa02074695b805545d4a9166aa8232 Mon Sep 17 00:00:00 2001 From: Ahn Kiwook Date: Wed, 18 Feb 2026 15:31:29 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=EC=9D=B4=EB=AA=A8=EC=A7=80=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=EA=B8=B0=20=EC=A0=84=ED=99=98=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/pe/aioo/openmoa/IMEMode.kt | 1 + .../main/kotlin/pe/aioo/openmoa/OpenMoaIME.kt | 30 ++++++++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/pe/aioo/openmoa/IMEMode.kt b/app/src/main/kotlin/pe/aioo/openmoa/IMEMode.kt index b6c902c..4fcb680 100644 --- a/app/src/main/kotlin/pe/aioo/openmoa/IMEMode.kt +++ b/app/src/main/kotlin/pe/aioo/openmoa/IMEMode.kt @@ -11,4 +11,5 @@ enum class IMEMode { IME_KO_PHONE, IME_KO_PUNCTUATION, IME_KO_NUMBER, + IME_EMOJI, } \ No newline at end of file diff --git a/app/src/main/kotlin/pe/aioo/openmoa/OpenMoaIME.kt b/app/src/main/kotlin/pe/aioo/openmoa/OpenMoaIME.kt index abeca59..e8d0b35 100644 --- a/app/src/main/kotlin/pe/aioo/openmoa/OpenMoaIME.kt +++ b/app/src/main/kotlin/pe/aioo/openmoa/OpenMoaIME.kt @@ -50,6 +50,7 @@ class OpenMoaIME : InputMethodService(), KoinComponent { private val config: Config by inject() private val hangulAssembler = HangulAssembler() private var imeMode = IMEMode.IME_KO + private var previousImeMode = IMEMode.IME_KO private var composingText = "" private fun finishComposing() { @@ -173,7 +174,8 @@ class OpenMoaIME : InputMethodService(), KoinComponent { IMEMode.IME_KO_PUNCTUATION, IMEMode.IME_KO_NUMBER, IMEMode.IME_KO_ARROW, - IMEMode.IME_KO_PHONE -> IMEMode.IME_KO + IMEMode.IME_KO_PHONE, + IMEMode.IME_EMOJI -> IMEMode.IME_KO IMEMode.IME_EN_PUNCTUATION, IMEMode.IME_EN_NUMBER, IMEMode.IME_EN_ARROW, @@ -187,7 +189,8 @@ class OpenMoaIME : InputMethodService(), KoinComponent { IMEMode.IME_KO, IMEMode.IME_KO_NUMBER, IMEMode.IME_KO_ARROW, - IMEMode.IME_KO_PHONE -> IMEMode.IME_KO_PUNCTUATION + IMEMode.IME_KO_PHONE, + IMEMode.IME_EMOJI -> IMEMode.IME_KO_PUNCTUATION IMEMode.IME_EN, IMEMode.IME_EN_NUMBER, IMEMode.IME_EN_ARROW, @@ -204,7 +207,8 @@ class OpenMoaIME : InputMethodService(), KoinComponent { IMEMode.IME_KO_NUMBER, IMEMode.IME_KO_PUNCTUATION, IMEMode.IME_KO_ARROW, - IMEMode.IME_KO_PHONE -> IMEMode.IME_KO_ARROW + IMEMode.IME_KO_PHONE, + IMEMode.IME_EMOJI -> IMEMode.IME_KO_ARROW IMEMode.IME_EN, IMEMode.IME_EN_NUMBER, IMEMode.IME_EN_PUNCTUATION, @@ -298,7 +302,14 @@ class OpenMoaIME : InputMethodService(), KoinComponent { KeyEvent.KEYCODE_MOVE_HOME, KeyEvent.META_CTRL_ON, true ) } - SpecialKey.EMOJI -> Unit + SpecialKey.EMOJI -> { + if (imeMode == IMEMode.IME_EMOJI) { + setKeyboard(previousImeMode) + } else { + previousImeMode = imeMode + setKeyboard(IMEMode.IME_EMOJI) + } + } } } is String -> { @@ -367,7 +378,8 @@ class OpenMoaIME : InputMethodService(), KoinComponent { IMEMode.IME_KO_PUNCTUATION, IMEMode.IME_KO_NUMBER, IMEMode.IME_KO_ARROW, - IMEMode.IME_KO_PHONE -> setKeyboard(IMEMode.IME_KO) + IMEMode.IME_KO_PHONE, + IMEMode.IME_EMOJI -> setKeyboard(IMEMode.IME_KO) IMEMode.IME_EN, IMEMode.IME_EN_PUNCTUATION, IMEMode.IME_EN_NUMBER, @@ -401,6 +413,7 @@ class OpenMoaIME : InputMethodService(), KoinComponent { val numberView = NumberView(this) val arrowView = ArrowView(this) val phoneView = PhoneView(this) + val emojiView = EmojiView(this) keyboardViews = mapOf( IMEMode.IME_KO to OpenMoaView(this), IMEMode.IME_EN to QuertyView(this), @@ -412,6 +425,7 @@ class OpenMoaIME : InputMethodService(), KoinComponent { IMEMode.IME_EN_ARROW to arrowView, IMEMode.IME_KO_PHONE to phoneView, IMEMode.IME_EN_PHONE to phoneView, + IMEMode.IME_EMOJI to emojiView, ) val view = layoutInflater.inflate(R.layout.open_moa_ime, null) binding = OpenMoaImeBinding.bind(view) @@ -430,7 +444,8 @@ class OpenMoaIME : InputMethodService(), KoinComponent { IMEMode.IME_KO_PUNCTUATION, IMEMode.IME_KO_NUMBER, IMEMode.IME_KO_ARROW, - IMEMode.IME_KO_PHONE -> IMEMode.IME_KO_NUMBER + IMEMode.IME_KO_PHONE, + IMEMode.IME_EMOJI -> IMEMode.IME_KO_NUMBER IMEMode.IME_EN, IMEMode.IME_EN_PUNCTUATION, IMEMode.IME_EN_NUMBER, @@ -446,7 +461,8 @@ class OpenMoaIME : InputMethodService(), KoinComponent { IMEMode.IME_KO_PUNCTUATION, IMEMode.IME_KO_NUMBER, IMEMode.IME_KO_ARROW, - IMEMode.IME_KO_PHONE -> IMEMode.IME_KO_PHONE + IMEMode.IME_KO_PHONE, + IMEMode.IME_EMOJI -> IMEMode.IME_KO_PHONE IMEMode.IME_EN, IMEMode.IME_EN_PUNCTUATION, IMEMode.IME_EN_NUMBER, From 966e7e37cb4b7513f367052d02f391cde5c8c4e6 Mon Sep 17 00:00:00 2001 From: Ahn Kiwook Date: Wed, 18 Feb 2026 15:35:11 +0900 Subject: [PATCH 5/5] =?UTF-8?q?CLAUDE.md=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3013fda --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,111 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when +working with code in this repository. + +## ํ”„๋กœ์ ํŠธ ๊ฐœ์š” + +**OpenMoa**๋Š” ์‚ผ์„ฑ ๋ชจ์•„ํ‚ค ํ•œ๊ตญ์–ด ํ‚ค๋ณด๋“œ๋ฅผ ์žฌ๊ตฌํ˜„ํ•œ ์˜คํ”ˆ์†Œ์Šค Android IME(์ž…๋ ฅ๊ธฐ)์ž…๋‹ˆ๋‹ค. +์ž์Œ ํ‚ค๋ฅผ ๋ˆ„๋ฅธ ์ฑ„ ๋ฐฉํ–ฅ์œผ๋กœ ๋“œ๋ž˜๊ทธํ•˜์—ฌ ๋ชจ์Œ์„ ์ž…๋ ฅํ•˜๋Š” ์ œ์Šค์ฒ˜ ๊ธฐ๋ฐ˜ ํ•œ๊ธ€ ์ž…๋ ฅ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. + +- ํŒจํ‚ค์ง€: `pe.aioo.openmoa` + +## ๋นŒ๋“œ ๋ฐ ํ…Œ์ŠคํŠธ ๋ช…๋ น์–ด + +```bash +# ์œ ๋‹› ํ…Œ์ŠคํŠธ ์‹คํ–‰ (CI์—์„œ๋„ ๋™์ผํ•˜๊ฒŒ ์‚ฌ์šฉ) +./gradlew testDebugUnitTest + +# ๊ธฐ๊ธฐ ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ ์‹คํ–‰ +./gradlew connectedAndroidTest + +# ๋””๋ฒ„๊ทธ APK ๋นŒ๋“œ +./gradlew assembleDebug +``` + +CI๋Š” GitHub Actions์—์„œ JDK 21 + Ubuntu ํ™˜๊ฒฝ์œผ๋กœ `testDebugUnitTest`๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + +## ์•„ํ‚คํ…์ฒ˜ + +### ํ•ต์‹ฌ ๋ ˆ์ด์–ด + +**1. IME ์„œ๋น„์Šค (`OpenMoaIME.kt`)** + +- `InputMethodService`๋ฅผ ์ƒ์†ํ•œ ๋ฉ”์ธ ์„œ๋น„์Šค +- 10๊ฐ€์ง€ ํ‚ค๋ณด๋“œ ๋ชจ๋“œ(`IMEMode` enum) ๊ด€๋ฆฌ: ํ•œ๊ตญ์–ด/์˜์–ด ร— ๊ธฐ๋ณธ/ํŠน์ˆ˜๋ฌธ์ž/์ˆซ์ž/๋ฐฉํ–ฅํ‚ค/์ „ํ™”๋ฒˆํ˜ธํŒจ๋“œ +- `KeyboardFrameLayout`์—์„œ ์˜ฌ๋ผ์˜ค๋Š” `BaseKeyMessage`(๋ฌธ์ž ๋˜๋Š” ํŠน์ˆ˜ํ‚ค) ์ˆ˜์‹  ํ›„ InputConnection์— ์ „๋‹ฌ +- Koin DI๋กœ `Config` ์ธ์Šคํ„ด์Šค๋ฅผ ์ฃผ์ž…๋ฐ›์Œ + +**2. ํ•œ๊ธ€ ์กฐํ•ฉ ์—”์ง„ (`hangul/`)** + +- `HangulAssembler`: ์ž์Œยท๋ชจ์Œ ์กฐํ•ฉ, ๋ณตํ•ฉ ์ž์Œ/๋ชจ์Œ ์ฒ˜๋ฆฌ, ์•„๋ž˜์•„(ใ†, แ†ข) ์‚ฌ์šฉ. +- HangulParser ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(์†Œ์Šค ํฌํ•จ)๋ฅผ ํ™œ์šฉํ•ด ์œ ํšจ์„ฑ ๊ฒ€์ฆ +- `MoeumGestureProcessor`: 8๋ฐฉํ–ฅ ์ œ์Šค์ฒ˜ ์‹œํ€€์Šค๋ฅผ ๋ชจ์Œ์œผ๋กœ ๋ณ€ํ™˜ + +**3. ๋ทฐ ๋ ˆ์ด์–ด (`view/`)** + +- `KeyboardFrameLayout`: ํ‚ค๋ณด๋“œ ๋ ˆ์ด์•„์›ƒ ์ „ํ™˜ ์ปจํ…Œ์ด๋„ˆ +- `keyboardview/`: + - `OpenMoaView`(ํ•œ๊ตญ์–ด ๋ชจ์•„ํ‚ค), `QuertyView`(์˜์–ด), + - `ArrowView`, `NumberView`, `PhoneView`, `PunctuationView` +- `keytouchlistener/`: ํ‚ค ์œ ํ˜•๋ณ„ ํ„ฐ์น˜ ํ•ธ๋“ค๋Ÿฌ (์•„๋ž˜ ์ฐธ์กฐ) + +**4. ํ„ฐ์น˜ ๋ฆฌ์Šค๋„ˆ ๊ณ„์ธต** + +- `BaseKeyTouchListener` (์ถ”์ƒ): ๋ชจ๋“  ๋ฆฌ์Šค๋„ˆ์˜ ๊ธฐ๋ฐ˜ +- `JaumKeyTouchListener`: ์ž์Œ ํ‚ค + ์ œ์Šค์ฒ˜ ๊ฐ์ง€ (`atan2` ๊ฐ๋„ ๊ณ„์‚ฐ, 50px ์ž„๊ณ„๊ฐ’) +- `FunctionalKeyTouchListener`: ์ƒํƒœ ๋ณ€๊ฒฝ ํ‚ค (shift, ๋ชจ๋“œ ์ „ํ™˜) +- `SimpleKeyTouchListener`: ๋‹จ์ˆœ ๋‹จ์ผ ๋™์ž‘ ํ‚ค +- `CrossKeyTouchListener`: ๋ฐฉํ–ฅํ‚ค +- `RepeatKeyTouchListener`: ๊ธธ๊ฒŒ ๋ˆ„๋ฅด๋ฉด ๋ฐ˜๋ณต๋˜๋Š” ํ‚ค + +### ํ•œ๊ธ€ ์ž…๋ ฅ ํ”Œ๋กœ์šฐ + +1. ์‚ฌ์šฉ์ž๊ฐ€ ์ž์Œ ํ‚ค ๋ˆ„๋ฆ„ โ†’ `JaumKeyTouchListener`๊ฐ€ ๋“œ๋ž˜๊ทธ ๋ฐฉํ–ฅ ๊ฐ์ง€ +2. `MoeumGestureProcessor`๊ฐ€ ์ œ์Šค์ฒ˜ ์‹œํ€€์Šค โ†’ ๋ชจ์Œ ๊ฒฐ์ • +3. `HangulAssembler`๊ฐ€ ์ž์Œ+๋ชจ์Œ ์กฐํ•ฉ โ†’ ์กฐํ•ฉ ์ค‘ ๋ฌธ์ž ํ‘œ์‹œ +4. ๋‹ค์Œ ์ž์Œ ์ž…๋ ฅ ๋˜๋Š” ์•ก์…˜ ํ‚ค โ†’ ๋ฌธ์ž ํ™•์ • ํ›„ InputConnection ์ „๋‹ฌ + +### IMEMode ํ™•์žฅ ์‹œ ์ฃผ์˜์‚ฌํ•ญ + +`IMEMode`์— ์ƒˆ ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ•˜๋ฉด `OpenMoaIME.kt` ๋‚ด ๋ชจ๋“  exhaustive `when (imeMode)` ๋ถ„๊ธฐ์— +์ผ€์ด์Šค๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ˆ„๋ฝ ์‹œ ์ปดํŒŒ์ผ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ๋Œ€์ƒ ์œ„์น˜: + +- `SpecialKey.LANGUAGE`, `HANJA_NUMBER_PUNCTUATION`, `ARROW` ์ฒ˜๋ฆฌ +- `onStartInputView()` ๋‚ด `TYPE_CLASS_NUMBER`, `TYPE_CLASS_PHONE` ๋ถ„๊ธฐ +- `returnFromNonStringKeyboard()` + +ํŠน์ • ํ‚ค๋ณด๋“œ์—์„œ๋งŒ ์ง„์ž… ๊ฐ€๋Šฅํ•œ ๋ชจ๋“œ๋Š” ํ•ด๋‹น ์–ธ์–ด ๊ณ„์—ด ์กฐ๊ฑด๊ณผ ์‰ผํ‘œ(,)๋กœ ๋ฌถ์–ด ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + +์˜ˆ) `IME_EMOJI`๋Š” ํ•œ๊ตญ์–ด ํ‚ค๋ณด๋“œ(`OpenMoaView`)์—์„œ๋งŒ ์ง„์ž… ๊ฐ€๋Šฅํ•˜๋ฏ€๋กœ, +๋Œ€๋ถ€๋ถ„์˜ ์œ„ ๋ถ„๊ธฐ์—์„œ `IME_KO_*` ์กฐ๊ฑด๋“ค๊ณผ ํ•จ๊ป˜ ๋ฌถ์–ด ๋ณ„๋„ ๋ถ„๊ธฐ ์—†์ด ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + +### ๋ฉ”์‹œ์ง€ ์‹œ์Šคํ…œ + +ํ‚ค ์ด๋ฒคํŠธ๋Š” `LocalBroadcastManager`๋ฅผ ํ†ตํ•ด ์ „๋‹ฌ: + +- `StringKeyMessage`: ๋ฌธ์ž ํ‚ค (์ผ๋ฐ˜ ๋ฌธ์ž) +- `SpecialKeyMessage`: ํŠน์ˆ˜ ๋™์ž‘ (`SpecialKey` enum - 27๊ฐ€์ง€: BACKSPACE, + ENTER, LANGUAGE, ๋ฐฉํ–ฅํ‚ค, COPY/CUT/PASTE ๋“ฑ) + +### ์„ค์ • (`Config`) + +Koin์œผ๋กœ ์‹ฑ๊ธ€ํ„ด ์ œ๊ณต: + +- `longPressRepeatTime`: 50ms +- `longPressThresholdTime`: 500ms +- `gestureThreshold`: 50px +- `hapticFeedback`: true +- `maxSuggestionCount`: 10 + +## ์ฃผ์š” ํŒŒ์ผ ์œ„์น˜ + +| ํŒŒ์ผ | ์—ญํ•  | +|------|------| +| `app/src/main/kotlin/pe/aioo/openmoa/OpenMoaIME.kt` | ๋ฉ”์ธ IME ์„œ๋น„์Šค | +| `app/src/main/kotlin/pe/aioo/openmoa/hangul/HangulAssembler.kt` | ํ•œ๊ธ€ ์ž๋ชจ ์กฐํ•ฉ ์—”์ง„ | +| `app/src/main/kotlin/pe/aioo/openmoa/hangul/MoeumGestureProcessor.kt` | ์ œ์Šค์ฒ˜โ†’๋ชจ์Œ ๋ณ€ํ™˜ | +| `app/src/main/kotlin/pe/aioo/openmoa/view/keyboardview/OpenMoaView.kt` | ํ•œ๊ตญ์–ด ํ‚ค๋ณด๋“œ ๋ ˆ์ด์•„์›ƒ | +| `app/src/main/kotlin/pe/aioo/openmoa/view/keytouchlistener/JaumKeyTouchListener.kt` | ์ž์Œ+์ œ์Šค์ฒ˜ ํ„ฐ์น˜ ์ฒ˜๋ฆฌ | +| `app/src/main/kotlin/pe/aioo/openmoa/config/Config.kt` | ์„ค์ • ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค | +| `app/src/main/res/values/strings.xml` | ๋ชจ๋“  UI ๋ฌธ์ž์—ด (ํ•œ๊ตญ์–ด) |