-
Notifications
You must be signed in to change notification settings - Fork 17
[1단계 - 칸반 보드 생성(상품 목록)] 별터 미션 제출합니다. #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: todays-sun-day
Are you sure you want to change the base?
Changes from all commits
1eefa7a
1103f10
b3d8ac5
0c08321
da55d2e
a98bcbc
b30ae69
476f27e
131599d
d4a5700
740fb54
dae5957
ece2e2f
1621414
d7a87a2
4212621
e566d9f
eaa5e6a
41598e4
ffbffa0
8f6a304
f46302a
5acc752
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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단계 | ||
| - 프로덕션 | ||
| - 테스트 |
| 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>, | ||
| val manager: String, | ||
| ) { | ||
| companion object { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://kotlinlang.org/docs/coding-conventions.html#class-layout 이 문서를 보고 위치를 다시 고민해보시기 바랍니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
이 질문에 다 답변을 해주시면 되며, 해당 답변을 하다보면 지금 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 어떠한 정보는 팩토리에서 검증을 하고 어떤 것은 외부에 검증을 맡기고 있네요?
Comment on lines
+35
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| 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() | ||
| } | ||
There was a problem hiding this comment.
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는 책임이 많다는 이유 하나로 분리해도 되는지가 고민이 되었습니다. 어떻게 하면 좋을지 리뷰어의 의견이 궁금합니다 !There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋은 고민인데요. 한 가지 기준을 생각해보면 좋겠습니다.
현재
Tag관련 로직은 파싱, 유효성 검사, 개수 제한, 길이 제한, 에러 메시지 등 여러 가지가 있잖아요. 반면title은 빈 문자열 체크 정도고요.만약 태그에 새로운 규칙이 추가된다면 (예: 특수문자 금지, 중복 태그 방지), 지금 구조에서는 어디를 수정해야 할까요? 수정해야 할 곳이 여러 군데라면, 그게 분리의 신호일 수 있습니다.
지금 당장 분리하지 않아도 괜찮지만, 별터만의 기준을 세워보시면 좋겠습니다.