create-ait-cmp-app 0.0.0-stage → 1.0.0-alpha02

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +31 -2
  2. package/cli.js +268 -0
  3. package/package.json +29 -3
  4. package/templates/app/.editorconfig +15 -0
  5. package/templates/app/LICENSE +21 -0
  6. package/templates/app/README.md +25 -0
  7. package/templates/app/_github/workflows/ci.yml +56 -0
  8. package/templates/app/_gitignore +11 -0
  9. package/templates/app/_yarn.lock +2576 -0
  10. package/templates/app/_yarnrc.yml +1 -0
  11. package/templates/app/androidApp/README.md +19 -0
  12. package/templates/app/androidApp/build.gradle.kts +29 -0
  13. package/templates/app/androidApp/src/main/AndroidManifest.xml +15 -0
  14. package/templates/app/androidApp/src/main/kotlin/io/github/l2hyunwoo/ait/sample/android/MainActivity.kt +49 -0
  15. package/templates/app/apps-in-toss.config.ts +2 -0
  16. package/templates/app/appsInTossApp/README.md +23 -0
  17. package/templates/app/appsInTossApp/build.gradle.kts +34 -0
  18. package/templates/app/appsInTossApp/src/webMain/kotlin/BrowserDraftStorage.kt +9 -0
  19. package/templates/app/appsInTossApp/src/webMain/kotlin/Main.kt +68 -0
  20. package/templates/app/appsInTossApp/src/webMain/resources/index.html +4 -0
  21. package/templates/app/build.gradle.kts +37 -0
  22. package/templates/app/docs/development.md +56 -0
  23. package/templates/app/eslint.config.mjs +19 -0
  24. package/templates/app/gradle/libs.versions.toml +31 -0
  25. package/templates/app/gradle/wrapper/gradle-wrapper.jar +0 -0
  26. package/templates/app/gradle/wrapper/gradle-wrapper.properties +10 -0
  27. package/templates/app/gradle.properties +3 -0
  28. package/templates/app/gradlew +248 -0
  29. package/templates/app/gradlew.bat +112 -0
  30. package/templates/app/iosApp/README.md +17 -0
  31. package/templates/app/iosApp/ServiceSample.xcodeproj/project.pbxproj +323 -0
  32. package/templates/app/iosApp/ServiceSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  33. package/templates/app/iosApp/ServiceSample.xcodeproj/xcshareddata/xcschemes/ServiceSample.xcscheme +91 -0
  34. package/templates/app/iosApp/iosApp/ServiceSampleApp.swift +20 -0
  35. package/templates/app/licenses/NotoSansKR-OFL.txt +93 -0
  36. package/templates/app/package.json +31 -0
  37. package/templates/app/scripts/lint-kotlin.mjs +8 -0
  38. package/templates/app/scripts/web.mjs +52 -0
  39. package/templates/app/settings.gradle.kts +17 -0
  40. package/templates/app/shared-core/README.md +19 -0
  41. package/templates/app/shared-core/build.gradle.kts +37 -0
  42. package/templates/app/shared-core/src/commonMain/kotlin/io/github/l2hyunwoo/ait/sample/Notes.kt +37 -0
  43. package/templates/app/shared-core/src/commonTest/kotlin/io/github/l2hyunwoo/ait/sample/NotesApiClientTest.kt +17 -0
  44. package/templates/app/shared-core/src/jsMain/kotlin/io/github/l2hyunwoo/ait/sample/BrowserNote.kt +12 -0
  45. package/templates/app/shared-ui/README.md +22 -0
  46. package/templates/app/shared-ui/build.gradle.kts +49 -0
  47. package/templates/app/shared-ui/src/commonMain/composeResources/files/NotoSansKR-OFL.txt +93 -0
  48. package/templates/app/shared-ui/src/commonMain/composeResources/font/NotoSansKR.ttf +0 -0
  49. package/templates/app/shared-ui/src/commonMain/kotlin/io/github/l2hyunwoo/ait/sample/ServiceApp.kt +121 -0
  50. package/templates/app/shared-ui/src/iosMain/kotlin/io/github/l2hyunwoo/ait/sample/MainViewController.kt +40 -0
  51. package/templates/app/webApp/README.md +18 -0
  52. package/templates/app/webApp/build.gradle.kts +23 -0
  53. package/templates/app/webApp/src/webMain/kotlin/BrowserDraftStorage.kt +9 -0
  54. package/templates/app/webApp/src/webMain/kotlin/Main.kt +8 -0
  55. package/templates/app/webApp/src/webMain/resources/index.html +4 -0
@@ -0,0 +1,37 @@
1
+ import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
2
+
3
+ plugins {
4
+ alias(libs.plugins.kotlin.multiplatform)
5
+ alias(libs.plugins.kotlin.serialization)
6
+ alias(libs.plugins.android.kotlin.multiplatform.library) apply false
7
+ }
8
+ val hasAndroid = rootProject.file("androidApp").isDirectory
9
+ val hasIos = rootProject.file("iosApp").isDirectory
10
+ if (hasAndroid) pluginManager.apply("com.android.kotlin.multiplatform.library")
11
+
12
+ kotlin {
13
+ targets.withType<KotlinMultiplatformAndroidLibraryTarget>().configureEach {
14
+ namespace = "__PACKAGE_NAME__.core"
15
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
16
+ minSdk = 24
17
+ compilerOptions { jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) }
18
+ }
19
+ if (hasIos) {
20
+ iosArm64()
21
+ iosSimulatorArm64()
22
+ }
23
+ jvm()
24
+ js {
25
+ browser()
26
+ binaries.library()
27
+ generateTypeScriptDefinitions()
28
+ }
29
+ wasmJs { browser() }
30
+ sourceSets {
31
+ commonMain.dependencies { implementation(libs.serialization.json) }
32
+ commonTest.dependencies {
33
+ implementation(kotlin("test"))
34
+ implementation(libs.coroutines.test)
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,37 @@
1
+ package __PACKAGE_NAME__
2
+
3
+ import kotlinx.serialization.Serializable
4
+ import kotlinx.serialization.json.Json
5
+
6
+ @Serializable
7
+ data class Note(val id: String, val title: String, val body: String)
8
+
9
+ interface NotesRepository {
10
+ suspend fun list(): List<Note>
11
+ }
12
+
13
+ // The transport owns HTTPS, authentication and platform-specific networking.
14
+ class NotesApiClient(private val getNotesJson: suspend () -> String) : NotesRepository {
15
+ override suspend fun list(): List<Note> = parseNotes(getNotesJson())
16
+ }
17
+
18
+ class DemoNotesRepository : NotesRepository {
19
+ override suspend fun list() = listOf(
20
+ Note("welcome", "공유 화면을 만들어요", "이 화면은 앱인토스, 웹, Android, iOS에서 함께 사용해요."),
21
+ Note("boundary", "필요한 부분만 가져가요", "토스 전용 기능과 디자인은 앱인토스에 남겨요."),
22
+ )
23
+ }
24
+
25
+ interface DraftStorage {
26
+ fun read(id: String): String?
27
+ fun write(id: String, body: String)
28
+ }
29
+ class MemoryDraftStorage : DraftStorage {
30
+ private val drafts = mutableMapOf<String, String>()
31
+ override fun read(id: String) = drafts[id]
32
+ override fun write(id: String, body: String) {
33
+ drafts[id] = body
34
+ }
35
+ }
36
+
37
+ internal fun parseNotes(json: String): List<Note> = Json.decodeFromString(json)
@@ -0,0 +1,17 @@
1
+ package __PACKAGE_NAME__
2
+
3
+ import kotlin.test.Test
4
+ import kotlin.test.assertEquals
5
+ import kotlin.test.assertFails
6
+ import kotlinx.coroutines.test.runTest
7
+
8
+ class NotesApiClientTest {
9
+ @Test fun decodesWithoutUiOrToss() = runTest {
10
+ val api = NotesApiClient { """[{"id":"1","title":"한글","body":"내용"}]""" }
11
+ assertEquals("한글", api.list().single().title)
12
+ }
13
+
14
+ @Test fun rejectsInvalidServerData() = runTest {
15
+ assertFails { NotesApiClient { """[{"id":null}]""" }.list() }
16
+ }
17
+ }
@@ -0,0 +1,12 @@
1
+ @file:OptIn(kotlin.js.ExperimentalJsExport::class)
2
+
3
+ package __PACKAGE_NAME__
4
+
5
+ import kotlin.js.JsExport
6
+
7
+ @JsExport
8
+ class BrowserNote(val id: String, val title: String, val body: String)
9
+
10
+ /** Supply your own fetch/auth transport; parsing uses the shared service model. */
11
+ @JsExport
12
+ fun decodeNotesResponse(json: String): Array<BrowserNote> = parseNotes(json).map { BrowserNote(it.id, it.title, it.body) }.toTypedArray()
@@ -0,0 +1,22 @@
1
+ # Shared UI
2
+
3
+ 메모 목록·상세·편집 화면을 Compose Multiplatform으로 제공하는 라이브러리입니다. 앱인토스·Android·iOS·독립 웹에서 같은 화면을 사용할 수 있습니다.
4
+
5
+ ## 화면과 상태
6
+
7
+ | API | 역할 |
8
+ | --- | --- |
9
+ | `ServiceApp(...)` | 목록·상세 흐름, 데이터 조회, 저장·공유 동작 구성 |
10
+ | `NotesList(notes, onSelect, modifier)` | 메모 목록 화면 |
11
+ | `NoteDetail(note, modifier, storage, onShare)` | 메모 편집·저장·공유 화면 |
12
+ | `ServiceNavigation` | 선택한 메모와 목록 복귀 상태 |
13
+
14
+ 필요한 화면만 사용하거나 `ServiceApp`으로 함께 구성합니다. `NotesRepository`와 `DraftStorage`는 [공통 core](../shared-core/README.md)의 계약입니다. 기본 조회 구현은 데모 데이터를 반환하고 기본 저장은 메모리를 사용합니다.
15
+
16
+ `onShare`에는 호스트의 공유 동작을, `hostContent`에는 호스트 전용 UI를 전달합니다. `showBackButton`으로 공통 화면의 목록 복귀 버튼을 제어합니다. 저장은 사용자가 `저장해요`를 누를 때 수행합니다.
17
+
18
+ ## 디자인
19
+
20
+ Compose Material 3 테마와 Noto Sans KR을 사용합니다. 토스·TDS 의존성은 없습니다. 한글 폰트를 리소스에 포함하며 [폰트 라이선스](src/commonMain/composeResources/files/NotoSansKR-OFL.txt)를 함께 배포합니다.
21
+
22
+ Android·iOS·웹 호스트는 플랫폼의 저장·공유·생명주기를 연결합니다. 화면의 재사용과 계정·인증·서버 동기화는 별도 책임입니다.
@@ -0,0 +1,49 @@
1
+ import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
2
+
3
+ plugins {
4
+ alias(libs.plugins.kotlin.multiplatform)
5
+ alias(libs.plugins.kotlin.compose)
6
+ alias(libs.plugins.compose)
7
+ alias(libs.plugins.android.kotlin.multiplatform.library) apply false
8
+ }
9
+ val hasAndroid = rootProject.file("androidApp").isDirectory
10
+ val hasIos = rootProject.file("iosApp").isDirectory
11
+ if (hasAndroid) pluginManager.apply("com.android.kotlin.multiplatform.library")
12
+
13
+ kotlin {
14
+ targets.withType<KotlinMultiplatformAndroidLibraryTarget>().configureEach {
15
+ namespace = "__PACKAGE_NAME__.ui"
16
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
17
+ minSdk = 24
18
+ compilerOptions { jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) }
19
+ androidResources { enable = true }
20
+ }
21
+ if (hasIos) {
22
+ iosArm64()
23
+ iosSimulatorArm64()
24
+ }
25
+ targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget>().configureEach {
26
+ binaries.framework {
27
+ baseName = "SharedUi"
28
+ isStatic = true
29
+ }
30
+ }
31
+ js { browser() }
32
+ wasmJs { browser() }
33
+ sourceSets {
34
+ commonMain.dependencies {
35
+ api(project(":shared-core"))
36
+ implementation(libs.compose.resources)
37
+ implementation(libs.compose.runtime)
38
+ implementation(libs.compose.foundation)
39
+ implementation(libs.compose.material3)
40
+ implementation(libs.compose.ui)
41
+ implementation(libs.coroutines.core)
42
+ }
43
+ }
44
+ }
45
+
46
+ compose.resources {
47
+ publicResClass = true
48
+ packageOfResClass = "__PACKAGE_NAME__.resources"
49
+ }
@@ -0,0 +1,93 @@
1
+ Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ https://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,121 @@
1
+ package __PACKAGE_NAME__
2
+
3
+ import androidx.compose.foundation.clickable
4
+ import androidx.compose.foundation.layout.*
5
+ import androidx.compose.foundation.lazy.LazyColumn
6
+ import androidx.compose.foundation.lazy.items
7
+ import androidx.compose.material3.*
8
+ import androidx.compose.runtime.*
9
+ import androidx.compose.runtime.saveable.rememberSaveable
10
+ import androidx.compose.ui.Modifier
11
+ import androidx.compose.ui.text.font.FontFamily
12
+ import androidx.compose.ui.unit.dp
13
+ import __PACKAGE_NAME__.resources.NotoSansKR
14
+ import __PACKAGE_NAME__.resources.Res
15
+ import kotlinx.coroutines.CancellationException
16
+ import org.jetbrains.compose.resources.Font
17
+
18
+ class ServiceNavigation {
19
+ var selectedId by mutableStateOf<String?>(null)
20
+ fun back(): Boolean {
21
+ if (selectedId == null) return false
22
+ selectedId = null
23
+ return true
24
+ }
25
+ }
26
+
27
+ @Composable
28
+ fun ServiceApp(
29
+ state: ServiceNavigation = remember { ServiceNavigation() },
30
+ storage: DraftStorage = remember { MemoryDraftStorage() },
31
+ onShare: ((String) -> Unit)? = null,
32
+ showBackButton: Boolean = true,
33
+ repository: NotesRepository = remember { DemoNotesRepository() },
34
+ hostContent: @Composable () -> Unit = {},
35
+ ) {
36
+ var notes by remember { mutableStateOf<List<Note>>(emptyList()) }
37
+ var error by remember { mutableStateOf<String?>(null) }
38
+ LaunchedEffect(repository) {
39
+ try {
40
+ notes = repository.list()
41
+ } catch (e: CancellationException) {
42
+ throw e
43
+ } catch (_: Exception) {
44
+ error = "목록을 불러오지 못했어요."
45
+ }
46
+ }
47
+ MaterialTheme(colorScheme = lightColorScheme(), typography = serviceTypography()) {
48
+ Surface(Modifier.fillMaxSize()) {
49
+ Column(Modifier.safeDrawingPadding().imePadding().padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
50
+ Text("__DISPLAY_NAME__", style = MaterialTheme.typography.headlineMedium)
51
+ Text("데모 데이터예요. 필요한 화면과 API를 함께 사용해요.")
52
+ val selected = notes.firstOrNull { it.id == state.selectedId }
53
+ if (selected == null) {
54
+ error?.let { Text(it) }
55
+ NotesList(notes, { state.selectedId = it }, Modifier.weight(1f))
56
+ } else {
57
+ if (showBackButton) TextButton(onClick = { state.back() }) { Text("목록으로 돌아가요") }
58
+ NoteDetail(selected, Modifier.weight(1f), storage, onShare)
59
+ }
60
+ hostContent()
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ @Composable
67
+ fun NotesList(notes: List<Note>, onSelect: (String) -> Unit, modifier: Modifier = Modifier) {
68
+ LazyColumn(modifier, verticalArrangement = Arrangement.spacedBy(12.dp)) {
69
+ items(notes, key = { it.id }) { note ->
70
+ Card(Modifier.fillMaxWidth().clickable(onClickLabel = "상세 보기", onClick = { onSelect(note.id) })) {
71
+ Text(note.title, Modifier.padding(20.dp))
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ @Composable
78
+ fun NoteDetail(note: Note, modifier: Modifier = Modifier, storage: DraftStorage = remember { MemoryDraftStorage() }, onShare: ((String) -> Unit)? = null) {
79
+ val loaded = remember(note.id, storage) { runCatching { storage.read(note.id) } }
80
+ var draft by rememberSaveable(note.id) { mutableStateOf(loaded.getOrNull() ?: note.body) }
81
+ var message by remember(note.id) { mutableStateOf<String?>(null) }
82
+ Column(modifier, verticalArrangement = Arrangement.spacedBy(12.dp)) {
83
+ Text(note.title, style = MaterialTheme.typography.titleLarge)
84
+ OutlinedTextField(draft, { draft = it }, label = { Text("내용을 입력해요") }, modifier = Modifier.fillMaxWidth())
85
+ if (loaded.isFailure) Text("저장된 내용을 불러오지 못했어요. 다시 열어 주세요.")
86
+ Button(enabled = loaded.isSuccess, onClick = {
87
+ message = try {
88
+ storage.write(note.id, draft)
89
+ "저장했어요."
90
+ } catch (_: Exception) {
91
+ "저장하지 못했어요. 다시 시도해 주세요."
92
+ }
93
+ }) { Text("저장해요") }
94
+ message?.let { Text(it) }
95
+ onShare?.let { share -> TextButton(onClick = { share(draft) }, enabled = draft.isNotBlank()) { Text("내용을 공유해요") } }
96
+ Text("데모는 기기 안에 저장해요. 민감 정보는 입력하지 마세요.")
97
+ }
98
+ }
99
+
100
+ @Composable
101
+ private fun serviceTypography(): Typography {
102
+ val family = FontFamily(Font(Res.font.NotoSansKR))
103
+ val base = Typography()
104
+ return base.copy(
105
+ displayLarge = base.displayLarge.copy(fontFamily = family),
106
+ displayMedium = base.displayMedium.copy(fontFamily = family),
107
+ displaySmall = base.displaySmall.copy(fontFamily = family),
108
+ headlineLarge = base.headlineLarge.copy(fontFamily = family),
109
+ headlineMedium = base.headlineMedium.copy(fontFamily = family),
110
+ headlineSmall = base.headlineSmall.copy(fontFamily = family),
111
+ titleLarge = base.titleLarge.copy(fontFamily = family),
112
+ titleMedium = base.titleMedium.copy(fontFamily = family),
113
+ titleSmall = base.titleSmall.copy(fontFamily = family),
114
+ bodyLarge = base.bodyLarge.copy(fontFamily = family),
115
+ bodyMedium = base.bodyMedium.copy(fontFamily = family),
116
+ bodySmall = base.bodySmall.copy(fontFamily = family),
117
+ labelLarge = base.labelLarge.copy(fontFamily = family),
118
+ labelMedium = base.labelMedium.copy(fontFamily = family),
119
+ labelSmall = base.labelSmall.copy(fontFamily = family),
120
+ )
121
+ }
@@ -0,0 +1,40 @@
1
+ @file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, kotlin.experimental.ExperimentalNativeApi::class)
2
+
3
+ package __PACKAGE_NAME__
4
+
5
+ import androidx.compose.runtime.remember
6
+ import androidx.compose.ui.window.ComposeUIViewController
7
+ import kotlin.native.ref.WeakReference
8
+ import platform.Foundation.NSUserDefaults
9
+ import platform.UIKit.UIActivityViewController
10
+ import platform.UIKit.UIViewController
11
+ import platform.UIKit.popoverPresentationController
12
+
13
+ @Suppress("ktlint:standard:function-naming")
14
+ fun MainViewController(): UIViewController {
15
+ var host: WeakReference<UIViewController>? = null
16
+ val controller = ComposeUIViewController {
17
+ val storage = remember { UserDefaultsDraftStorage(NSUserDefaults.standardUserDefaults) }
18
+ ServiceApp(storage = storage, onShare = { text ->
19
+ val presenter = host?.get()
20
+ if (presenter != null && presenter.presentedViewController == null) {
21
+ val activity = UIActivityViewController(activityItems = listOf(text), applicationActivities = null)
22
+ activity.popoverPresentationController?.let { popover ->
23
+ popover.sourceView = presenter.view
24
+ popover.sourceRect = presenter.view.bounds
25
+ }
26
+ presenter.presentViewController(activity, animated = true, completion = null)
27
+ }
28
+ })
29
+ }
30
+ host = WeakReference(controller)
31
+ return controller
32
+ }
33
+
34
+ private class UserDefaultsDraftStorage(private val defaults: NSUserDefaults) : DraftStorage {
35
+ override fun read(id: String): String? = defaults.stringForKey("draft:$id")
36
+
37
+ override fun write(id: String, body: String) {
38
+ defaults.setObject(body, forKey = "draft:$id")
39
+ }
40
+ }
@@ -0,0 +1,18 @@
1
+ # 독립 웹 메모 앱
2
+
3
+ 토스 런타임 없이 공통 메모 화면과 서비스 로직을 사용하는 Compose 웹앱 예제입니다. [공통 UI](../shared-ui/README.md)와 [공통 core](../shared-core/README.md)를 사용합니다.
4
+
5
+ ## 동작
6
+
7
+ 메모 목록·상세·편집·저장을 제공합니다. 초안은 localStorage에 메모 ID별로 저장하며 출처(origin)별로 구분합니다. 토스 로그인, 브리지, mock은 배포물에 포함하지 않습니다. URL·히스토리 기반 화면 이동과 서비스 인증은 별도 구현이 필요합니다.
8
+
9
+ ## 실행
10
+
11
+ Node 24 이상·Corepack·JDK 17과 Yarn 의존성을 준비하고 프로젝트 루트에서 실행합니다.
12
+
13
+ ```sh
14
+ yarn dev:web
15
+ yarn build:web
16
+ ```
17
+
18
+ 배포물은 `dist/web`에 생성됩니다. 저장소 예제의 조회 데이터는 로컬 데모 데이터입니다.
@@ -0,0 +1,23 @@
1
+ plugins {
2
+ alias(libs.plugins.kotlin.multiplatform)
3
+ alias(libs.plugins.kotlin.compose)
4
+ alias(libs.plugins.compose)
5
+ }
6
+ kotlin {
7
+ js {
8
+ browser()
9
+ binaries.executable()
10
+ }
11
+ wasmJs {
12
+ browser()
13
+ binaries.executable()
14
+ }
15
+ sourceSets {
16
+ commonMain.dependencies {
17
+ implementation(project(":shared-ui"))
18
+ implementation(libs.compose.ui)
19
+ implementation(libs.compose.material3)
20
+ implementation(libs.coroutines.core)
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,9 @@
1
+ import __PACKAGE_NAME__.DraftStorage
2
+ import kotlinx.browser.window
3
+
4
+ class BrowserDraftStorage : DraftStorage {
5
+ override fun read(id: String): String? = window.localStorage.getItem("cmp-note-$id")
6
+ override fun write(id: String, body: String) {
7
+ window.localStorage.setItem("cmp-note-$id", body)
8
+ }
9
+ }
@@ -0,0 +1,8 @@
1
+ import androidx.compose.ui.ExperimentalComposeUiApi
2
+ import androidx.compose.ui.window.ComposeViewport
3
+ import __PACKAGE_NAME__.ServiceApp
4
+
5
+ @OptIn(ExperimentalComposeUiApi::class)
6
+ fun main() {
7
+ ComposeViewport(viewportContainerId = "compose-root") { ServiceApp(storage = androidx.compose.runtime.remember { BrowserDraftStorage() }) }
8
+ }
@@ -0,0 +1,4 @@
1
+ <!doctype html>
2
+ <html lang="ko"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>__DISPLAY_NAME__</title><style>html,body,#compose-root{margin:0;width:100%;height:100%;overflow:hidden}#error{padding:24px}</style></head>
3
+ <body><div id="compose-root"></div><div id="error" role="alert" hidden></div><script>window.addEventListener('error',()=>{const e=document.getElementById('error');e.hidden=false;e.textContent='화면을 불러오지 못했어요. 다시 열어 주세요.';});</script>
4
+ <script src="webApp.js"></script></body></html>