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
8f6a304
feat: CardCreationPanel UI 테스트 로직 작성
todays-sun-day Mar 11, 2026
f46302a
feat: Tag 유효성 검사 테스트 로직 작성
todays-sun-day Mar 11, 2026
5acc752
chore: 불필요한 import 제거
todays-sun-day 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
37 changes: 4 additions & 33 deletions composeApp/src/commonMain/kotlin/woowacourse/kanban/board/App.kt
Original file line number Diff line number Diff line change
@@ -1,44 +1,15 @@
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
import woowacourse.kanban.board.ui.Board

@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)
}
}
Board()
}
}
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>,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

태그를 Tag 클래스와 Tags 클래스로 만들어서 CardData의 책임을 분리할까 고민했습니다.
하지만 다른 title이나 content 등도 별도의 클래스가 없이 분리를 안 했는데, Tag는 책임이 많다는 이유 하나로 분리해도 되는지가 고민이 되었습니다. 어떻게 하면 좋을지 리뷰어의 의견이 궁금합니다 !

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋은 고민인데요. 한 가지 기준을 생각해보면 좋겠습니다.

현재 Tag 관련 로직은 파싱, 유효성 검사, 개수 제한, 길이 제한, 에러 메시지 등 여러 가지가 있잖아요. 반면 title은 빈 문자열 체크 정도고요.

만약 태그에 새로운 규칙이 추가된다면 (예: 특수문자 금지, 중복 태그 방지), 지금 구조에서는 어디를 수정해야 할까요? 수정해야 할 곳이 여러 군데라면, 그게 분리의 신호일 수 있습니다.

지금 당장 분리하지 않아도 괜찮지만, 별터만의 기준을 세워보시면 좋겠습니다.

val manager: String,
) {
companion object {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://kotlinlang.org/docs/coding-conventions.html#class-layout 이 문서를 보고 위치를 다시 고민해보시기 바랍니다.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. companion object 는 무엇인가요?
  2. companion object 는 어떠한 상황에 사용을 해야 하나요?
  3. companion object 는 java 기준으로 봤을 때 어떠한 역할을 하고 있을까요?

이 질문에 다 답변을 해주시면 되며, 해당 답변을 하다보면 지금 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)
}
Comment on lines +35 to +40

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어떠한 정보는 팩토리에서 검증을 하고 어떤 것은 외부에 검증을 맡기고 있네요? 검증 은 누구의 역할일까요?

Comment on lines +35 to +40

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 실제로 크래시가 난 사례이고 https://github.com/woowacourse/compose-kanban-board-create/pull/2/changes#r2918544382 이 코멘트랑도 구조적으로 관련이 있으니 같이 해결하시기 바랍니다.

========================================

사용자가 태그에 "우아한테크코스"(7글자)를 입력하면, 생성 버튼이 활성화될까요? 그리고 실제로 생성 버튼을 누르면 어떤 일이 벌어질까요?

근본적으로 "에러 발생 예시" 피그마에서 지금 "생성" 버튼이 어떤 상황인지도 확인해보시기 바랍니다. 그리고 피그마가 아니더라도 이런 기본적인 UX는 지켜야 합니다. 에러 상태인데 성공 생성 같은 버튼이 활성화가 되어서는 안됩니다.


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] 계정명은 필수 입력 항목입니다." }
Comment on lines +60 to +67

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 팩토리 메서드 패턴은 어느 상황에서 사용을 하면 좋을까요?
  2. 지금 구조를 똑같이 init 에서 검증을 하는 것과 어떠한 차이가 있나요?


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