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.
- package/README.md +31 -2
- package/cli.js +268 -0
- package/package.json +29 -3
- package/templates/app/.editorconfig +15 -0
- package/templates/app/LICENSE +21 -0
- package/templates/app/README.md +25 -0
- package/templates/app/_github/workflows/ci.yml +56 -0
- package/templates/app/_gitignore +11 -0
- package/templates/app/_yarn.lock +2576 -0
- package/templates/app/_yarnrc.yml +1 -0
- package/templates/app/androidApp/README.md +19 -0
- package/templates/app/androidApp/build.gradle.kts +29 -0
- package/templates/app/androidApp/src/main/AndroidManifest.xml +15 -0
- package/templates/app/androidApp/src/main/kotlin/io/github/l2hyunwoo/ait/sample/android/MainActivity.kt +49 -0
- package/templates/app/apps-in-toss.config.ts +2 -0
- package/templates/app/appsInTossApp/README.md +23 -0
- package/templates/app/appsInTossApp/build.gradle.kts +34 -0
- package/templates/app/appsInTossApp/src/webMain/kotlin/BrowserDraftStorage.kt +9 -0
- package/templates/app/appsInTossApp/src/webMain/kotlin/Main.kt +68 -0
- package/templates/app/appsInTossApp/src/webMain/resources/index.html +4 -0
- package/templates/app/build.gradle.kts +37 -0
- package/templates/app/docs/development.md +56 -0
- package/templates/app/eslint.config.mjs +19 -0
- package/templates/app/gradle/libs.versions.toml +31 -0
- package/templates/app/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/templates/app/gradle/wrapper/gradle-wrapper.properties +10 -0
- package/templates/app/gradle.properties +3 -0
- package/templates/app/gradlew +248 -0
- package/templates/app/gradlew.bat +112 -0
- package/templates/app/iosApp/README.md +17 -0
- package/templates/app/iosApp/ServiceSample.xcodeproj/project.pbxproj +323 -0
- package/templates/app/iosApp/ServiceSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/templates/app/iosApp/ServiceSample.xcodeproj/xcshareddata/xcschemes/ServiceSample.xcscheme +91 -0
- package/templates/app/iosApp/iosApp/ServiceSampleApp.swift +20 -0
- package/templates/app/licenses/NotoSansKR-OFL.txt +93 -0
- package/templates/app/package.json +31 -0
- package/templates/app/scripts/lint-kotlin.mjs +8 -0
- package/templates/app/scripts/web.mjs +52 -0
- package/templates/app/settings.gradle.kts +17 -0
- package/templates/app/shared-core/README.md +19 -0
- package/templates/app/shared-core/build.gradle.kts +37 -0
- package/templates/app/shared-core/src/commonMain/kotlin/io/github/l2hyunwoo/ait/sample/Notes.kt +37 -0
- package/templates/app/shared-core/src/commonTest/kotlin/io/github/l2hyunwoo/ait/sample/NotesApiClientTest.kt +17 -0
- package/templates/app/shared-core/src/jsMain/kotlin/io/github/l2hyunwoo/ait/sample/BrowserNote.kt +12 -0
- package/templates/app/shared-ui/README.md +22 -0
- package/templates/app/shared-ui/build.gradle.kts +49 -0
- package/templates/app/shared-ui/src/commonMain/composeResources/files/NotoSansKR-OFL.txt +93 -0
- package/templates/app/shared-ui/src/commonMain/composeResources/font/NotoSansKR.ttf +0 -0
- package/templates/app/shared-ui/src/commonMain/kotlin/io/github/l2hyunwoo/ait/sample/ServiceApp.kt +121 -0
- package/templates/app/shared-ui/src/iosMain/kotlin/io/github/l2hyunwoo/ait/sample/MainViewController.kt +40 -0
- package/templates/app/webApp/README.md +18 -0
- package/templates/app/webApp/build.gradle.kts +23 -0
- package/templates/app/webApp/src/webMain/kotlin/BrowserDraftStorage.kt +9 -0
- package/templates/app/webApp/src/webMain/kotlin/Main.kt +8 -0
- 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
|
+
}
|
package/templates/app/shared-core/src/commonMain/kotlin/io/github/l2hyunwoo/ait/sample/Notes.kt
ADDED
|
@@ -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
|
+
}
|
package/templates/app/shared-core/src/jsMain/kotlin/io/github/l2hyunwoo/ait/sample/BrowserNote.kt
ADDED
|
@@ -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.
|
|
Binary file
|
package/templates/app/shared-ui/src/commonMain/kotlin/io/github/l2hyunwoo/ait/sample/ServiceApp.kt
ADDED
|
@@ -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>
|