Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1eefa7a
chore : KanbanBoardCard 코드 마이그레이션
GiyunKim00 Mar 10, 2026
1103f10
docs : README 작성
GiyunKim00 Mar 10, 2026
b3d8ac5
fix : 코드 내 Card 명칭 수정
GiyunKim00 Mar 10, 2026
0c08321
feat : CardCreationPanel 헤더 섹션 작성
GiyunKim00 Mar 11, 2026
da55d2e
feat : CardCreationPanel 제목 입력 섹션 작성
GiyunKim00 Mar 11, 2026
a98bcbc
feat : CardCreationPanel 설명 입력 섹션 작성
GiyunKim00 Mar 11, 2026
b30ae69
feat : CardCreationPanel 태그 입력 섹션 작성
GiyunKim00 Mar 11, 2026
476f27e
feat : CardCreationPanel 상태 선택 버튼 작성
GiyunKim00 Mar 11, 2026
131599d
feat : CardCreationPanel 담당자 선택 버튼 작성
GiyunKim00 Mar 11, 2026
d4a5700
feat : CardCreationPanel 생성, 취소 버튼 작성
GiyunKim00 Mar 11, 2026
740fb54
docs : 피그마 디자인 가이드에 따른 UI/Domain 관련 추가사항 작성
GiyunKim00 Mar 11, 2026
dae5957
feat : 상태 로직에 따른 에러 메시지 추가
GiyunKim00 Mar 11, 2026
ece2e2f
feat : 제목 유효성 검증 로직 작성
GiyunKim00 Mar 11, 2026
1621414
feat : 태그 파싱 로직 작성
GiyunKim00 Mar 11, 2026
d7a87a2
feat : 정규화 태그 유효성 검증 로직 작성
GiyunKim00 Mar 11, 2026
4212621
feat : 태그 검증 결과에 따른 에러 메시지 반환 로직 작성
GiyunKim00 Mar 11, 2026
e566d9f
feat : KanbanBoard 새 테스크 작성 UI 구성
GiyunKim00 Mar 11, 2026
eaa5e6a
fix : 디자인 가이드에 따른 용어 변경
GiyunKim00 Mar 11, 2026
41598e4
feat : 제목 유효성 검증 결과에 따른 UI 로직 작성
GiyunKim00 Mar 11, 2026
ffbffa0
feat : 태그 및 버튼 유효겅 검증에 따른 UI 표현 로직 작성
GiyunKim00 Mar 11, 2026
1b0b976
feat : UI 및 도메인 테스트 코드 추가
GiyunKim00 Mar 11, 2026
088ac22
fix : Test 라이브러리 수정
GiyunKim00 Mar 11, 2026
9eb8844
fix : 1단계 범위 외 파일 제외
GiyunKim00 Mar 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 44 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,49 @@
This is a Kotlin Multiplatform project targeting Android, Desktop (JVM).
# 칸반 보드 생성
## 1단계 - 칸반 보드 생성(상품 목록)

* [/composeApp](./composeApp/src) is for code that will be shared across your Compose Multiplatform applications.
It contains several subfolders:
- [commonMain](./composeApp/src/commonMain/kotlin) is for code that’s common for all targets.
- Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name.
For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app,
the [iosMain](./composeApp/src/iosMain/kotlin) folder would be the right place for such calls.
Similarly, if you want to edit the Desktop (JVM) specific part, the [jvmMain](./composeApp/src/jvmMain/kotlin)
folder is the appropriate location.
## 요구 사항
### 기능 요구 사항
- [x] 필수 입력 란과 선택 입력 란을 구분한다.
- [x] 필수 입력: 제목, 상태, 담당자
- [x] 선택 입력: 설명, 태그
- [x] 상태와 담당자는 첫 번째 항목으로 기본 선택되어 있고, 한 항목만 선택 가능하다.
- [x] 유효성 검사가 실패하면 생성 버튼을 누를 수 없다.
- [x] 기존 코드를 마이그레이션한 뒤 하나의 커밋으로 합친다.
- [x] 페어와 협의에 따른 카드 명칭 변경

### Build and Run Android Application
### 프로그래밍 요구 사항
- ViewModel, Hilt 등은 장바구니 미션에서 활용하지 않는다. 컴포즈 학습에 집중하자.
- 컴포저블 함수가 너무 많은 일을 하지 않도록 분리하기 위해 노력해 본다.
- 디자인 정합성을 맞추기 위한 너무 많은 노력을 기울이지 않아도 된다.
- 1px 단위에 연연하지 말고, 폰트와 색상도 중요하지 않다.
- 단위 테스트만으로도 충분한 로직과, UI 테스트가 필요한 영역을 구분한다.
- 핵심 비즈니스 로직을 가지는 객체를 분리해 단위 테스트를 진행한다.
- Compose UI Testing을 활용하여 기능 요구 사항을 테스트한다.

To build and run the development version of the Android app, use the run configuration from the run widget
in your IDE’s toolbar or build it directly from the terminal:
- on macOS/Linux
```shell
./gradlew :composeApp:assembleDebug
```
- on Windows
```shell
.\gradlew.bat :composeApp:assembleDebug
```
## 구현할 기능
### UI
- [x] 헤더 섹션 작성
- [x] 제목 입력 필드 작성
- [x] 설명 입력 필드 작성
- [x] 태그 입력 필드 작성
- [x] 상태 선택 버튼 작성
- [x] 담당자 선택 버튼 작성
- [x] 생성, 취소 버튼 작성
- [x] KanbanBoard 새 태스크 생성 버튼 작성
- [x] Card 입력 UI 내 비즈니즈 로직 분리
- [x] 디자인 가이드에 따른 용어 변경
- [x] 제목 유효성 검증 결과에 따른 시각적 표현 추가
- [x] 태그 유효성 검증 결과에 따른 시각적 표현 추가
- [x] 테스크 유효성 검증 결과에 따른 버튼 활성화 로직 추가

### Build and Run Desktop (JVM) Application
### Domain
- [x] 상태 로직 추가
- [x] 제목 유효성 로직 작성
- [x] 태그 파싱 로직 작성
- [x] 정규화 태그 유효성 검증 로직 작성
- [x] 태그 검증 결과에 따른 에러 메시지 반환 로직 작성

To build and run the development version of the desktop app, use the run configuration from the run widget
in your IDE’s toolbar or run it directly from the terminal:
- on macOS/Linux
```shell
./gradlew :composeApp:run
```
- on Windows
```shell
.\gradlew.bat :composeApp:run
```

---

Learn more about [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html)
## 리팩토링
### 1단계
- 프로덕션
- 테스트
5 changes: 4 additions & 1 deletion composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ kotlin {
implementation(libs.compose.uiToolingPreview)
implementation(libs.androidx.lifecycle.viewmodelCompose)
implementation(libs.androidx.lifecycle.runtimeCompose)
implementation("org.jetbrains.compose.material:material-icons-extended:1.7.3")
}
commonTest.dependencies {
implementation(libs.kotlin.test)
implementation(libs.assertj.core)
@OptIn(ExperimentalComposeLibrary::class)
implementation(compose.uiTest)
Expand All @@ -45,6 +45,9 @@ kotlin {
implementation(libs.kotlinx.coroutinesSwing)
}
}
sourceSets.commonMain.dependencies {
implementation(kotlin("test"))
}
}

android {
Expand Down
38 changes: 3 additions & 35 deletions composeApp/src/commonMain/kotlin/woowacourse/kanban/board/App.kt
Original file line number Diff line number Diff line change
@@ -1,44 +1,12 @@
package woowacourse.kanban.board

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Devices.DESKTOP
import androidx.compose.ui.tooling.preview.Preview
import kanbanboard.composeapp.generated.resources.Res
import kanbanboard.composeapp.generated.resources.compose_multiplatform
import org.jetbrains.compose.resources.painterResource

@Composable
@Preview
@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF, device = DESKTOP)
fun App() {
MaterialTheme {
var showContent by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.primaryContainer)
.safeContentPadding()
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Button(onClick = { showContent = !showContent }) {
Text("Click me!")
}
AnimatedVisibility(showContent) {
Image(painterResource(Res.drawable.compose_multiplatform), null)
}
}
}
MaterialTheme {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package woowacourse.kanban.board.domain

/**
* Card 도메인 모델입니다.
* 카드 생성 규칙을 적용합니다.
* 생성은 [create] 팩토리 메서드로 수행합니다.
*/
class CardData private constructor(
val title: String,
val content: String,
val tags: List<String>,
val manager: String,
) {
companion object {
private const val MAX_TAG_COUNT = 5
private const val MAX_TAG_LENGTH = 5

private const val TITLE_INVALID_FORMAT_MSG = "제목을 입력해 주세요."
private const val TAG_VALID_FORMAT_MSG = "5자 이내의 태그를 최대 5개까지 등록할 수 있습니다."
private const val TAG_INVALID_FORMAT_MSG = "태그 형식이 올바르지 않습니다."
private const val TAG_INVALID_RULE_MSG = "태그는 5자 이내로 5개까지만 등록할 수 있습니다."

fun isValidText(rawText: String): Boolean {
return rawText.trim().isNotBlank()
}

fun getTitleInfo(): String {
return TITLE_INVALID_FORMAT_MSG
}

fun parseTag(tempTags: String): List<String> {
return tempTags.trim().split(",")
}

fun isValidTag(rawText: String): Boolean {
if (rawText.isBlank()) return true

val parsedText = parseTag(rawText)
return (parsedText.all { isValidText(it) } && parsedText.size <= MAX_TAG_COUNT)
}

fun isValidTagInfo(rawText: String): String {
val parsedText = parseTag(rawText)

if (isValidText(rawText) && parsedText.any { isValidText(it) == false }) return TAG_INVALID_FORMAT_MSG

if (isValidText(rawText) && parsedText.size > MAX_TAG_COUNT) return TAG_INVALID_RULE_MSG

return TAG_VALID_FORMAT_MSG
}

/**
* [CardData] 객체 생성 팩토리 메서드입니다.
* @param title 필수 | 제목
* @param content 본문
* @param tags 태그
* @param accountName 필수 | 계정명
* @throws IllegalArgumentException 기능 요구사항을 충족하지 않을 경우 예외를 던집니다.
*/
fun create(
title: String,
content: String,
tags: List<String>,
accountName: String,
): CardData {
require(title.isNotBlank()) { "[Card] 제목은 필수 입력 항목입니다." }
require(accountName.isNotBlank()) { "[Card] 계정명은 필수 입력 항목입니다." }

val normalizedTags = tags
.map { it.trim() }
.filter { it.isNotEmpty() }

require(normalizedTags.size <= MAX_TAG_COUNT) { "[Card] 태그는 최대 ${MAX_TAG_COUNT}개까지 가능합니다." }
require(normalizedTags.all { it.length <= MAX_TAG_LENGTH }) { "[Card] 태그는 최대 ${MAX_TAG_LENGTH}자까지 가능합니다." }

return CardData(
title = title,
content = content,
tags = normalizedTags,
manager = accountName,
)
}
}

/**
* 카드 내용 존재 여부를 리턴합니다.
* @return 내용이 공백이 아니면 true 리턴.
*/
fun hasContent(): Boolean = content.isNotBlank()

/**
* 태그 존재 여부를 리턴합니다
* @return 태그가 있다면 true 리턴.
*/
fun hasTag(): Boolean = tags.isNotEmpty()
}
Loading