From e999280a80a24a37f1ba33c8483181814a70da68 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Tue, 9 Dec 2025 17:15:47 +0900 Subject: [PATCH 01/93] #706 [feature] Add post create state --- .../presentation/screen/post/PostScreen.kt | 33 +++++++++++++++---- .../view/dialog/CommentBottomSheetDialog.kt | 29 +++++++++++++--- .../presentation/viewmodel/PostViewModel.kt | 20 ++++++----- 3 files changed, 62 insertions(+), 20 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/post/PostScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/post/PostScreen.kt index 18468f4df..8c4b27965 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/post/PostScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/post/PostScreen.kt @@ -99,7 +99,7 @@ fun PostScreen( val commentState = postViewModel.postComments.observeAsState() val commentText = remember { mutableStateOf(TextFieldValue("")) } val showMentionSearchView = remember { mutableStateOf(false) } - val commentFocusRequester = FocusRequester() + val commentFocusRequester = remember { FocusRequester() } // comment option val onClickCommentDelete: (Long) -> Unit = { commentId -> @@ -180,11 +180,30 @@ fun PostScreen( val onClickCancelReply: () -> Unit = { clearComment() } - val postCommentCreateSuccess by postViewModel.postCommentCreateSuccess.observeAsState(Event(false)) - if (postCommentCreateSuccess.getContentIfNotHandled() == true) { - clearComment() - keyboardController?.hide() - postViewModel.requestPostComment(postId) + + val postCommentCreateState by postViewModel.postCommentCreateState.observeAsState() + LaunchedEffect(postCommentCreateState) { + postCommentCreateState?.status?.let { state -> + when (state) { + Status.LOADING -> { + coroutineScope.launch { + snackBarHostState.showSnackbar(context.getString(R.string.loading_default_message)) + } + } + + Status.SUCCESS -> { + clearComment() + keyboardController?.hide() + postViewModel.requestPostComment(postId) + } + + Status.ERROR -> { + coroutineScope.launch { + snackBarHostState.showSnackbar(context.getString(R.string.network_error_dialog_default_message)) + } + } + } + } } BackHandler(enabled = loadingVisible) {} @@ -454,7 +473,7 @@ private fun PreviewPostScreen() { userSearchKeyword = userSearchKeyword, showMentionSearchView = showMentionSearchView, userResults = userResults, - commentFocusRequester = FocusRequester(), + commentFocusRequester = remember { FocusRequester() }, onClickPostComment = { }, onClickProfile = { }, onClickPost = { }, diff --git a/presentation/src/main/java/daily/dayo/presentation/view/dialog/CommentBottomSheetDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/dialog/CommentBottomSheetDialog.kt index 3984e606e..119f8970a 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/dialog/CommentBottomSheetDialog.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/dialog/CommentBottomSheetDialog.kt @@ -168,11 +168,30 @@ fun CommentBottomSheetDialog( val onClickCancelReply: () -> Unit = { clearComment() } - val postCommentCreateSuccess by postViewModel.postCommentCreateSuccess.observeAsState(Event(false)) - if (postCommentCreateSuccess.getContentIfNotHandled() == true) { - clearComment() - keyboardController?.hide() - postViewModel.requestPostComment(postId) + + val postCommentCreateState by postViewModel.postCommentCreateState.observeAsState() + LaunchedEffect(postCommentCreateState) { + postCommentCreateState?.status?.let { state -> + when (state) { + Status.LOADING -> { + coroutineScope.launch { + snackBarHostState.showSnackbar(context.getString(R.string.loading_default_message)) + } + } + + Status.SUCCESS -> { + clearComment() + keyboardController?.hide() + postViewModel.requestPostComment(postId) + } + + Status.ERROR -> { + coroutineScope.launch { + snackBarHostState.showSnackbar(context.getString(R.string.network_error_dialog_default_message)) + } + } + } + } } Surface( diff --git a/presentation/src/main/java/daily/dayo/presentation/viewmodel/PostViewModel.kt b/presentation/src/main/java/daily/dayo/presentation/viewmodel/PostViewModel.kt index 8c5a5affa..e0fccb7e5 100644 --- a/presentation/src/main/java/daily/dayo/presentation/viewmodel/PostViewModel.kt +++ b/presentation/src/main/java/daily/dayo/presentation/viewmodel/PostViewModel.kt @@ -66,8 +66,8 @@ class PostViewModel @Inject constructor( private val _postDeleteSuccess = MutableSharedFlow() val postDeleteSuccess = _postDeleteSuccess.asSharedFlow() - private val _postCommentCreateSuccess = MutableLiveData>() - val postCommentCreateSuccess get() = _postCommentCreateSuccess + private val _postCommentCreateState = MutableLiveData>() + val postCommentCreateState: LiveData> get() = _postCommentCreateState private val _postCommentDeleteSuccess = MutableLiveData>() val postCommentDeleteSuccess get() = _postCommentDeleteSuccess @@ -241,17 +241,19 @@ class PostViewModel @Inject constructor( } fun requestCreatePostComment(contents: String, postId: Long, mentionedUser: List) { - if (contents.isEmpty()) return + if (contents.isEmpty() || _postCommentCreateState.value?.status == Status.LOADING) return + viewModelScope.launch { + _postCommentCreateState.postValue(Resource.loading(null)) val mentionList = getMentionList(contents, mentionedUser) requestCreatePostCommentUseCase(contents = contents, postId = postId, mentionList = mentionList).let { response -> when (response) { is NetworkResponse.Success -> { - _postCommentCreateSuccess.postValue(Event(true)) + _postCommentCreateState.postValue(Resource.success(true)) } else -> { - _postCommentCreateSuccess.postValue(Event(false)) + _postCommentCreateState.postValue(Resource.error("댓글 작성 실패", false)) } } } @@ -259,19 +261,21 @@ class PostViewModel @Inject constructor( } fun requestCreatePostCommentReply(reply: Pair, contents: String, postId: Long, mentionedUser: List) { - if (contents.isEmpty()) return + if (contents.isEmpty() || _postCommentCreateState.value?.status == Status.LOADING) return + viewModelScope.launch { + _postCommentCreateState.postValue(Resource.loading(null)) val mentionList = getMentionList(contents, mentionedUser).toMutableList() val (parentCommentId, comment) = reply mentionList.add(MentionUser(comment.memberId, comment.nickname)) // 언급된 유저 리스트에 원본 댓글 유저 추가 (팔로우하지 않아도 답글 가능하므로 따로 추가) requestCreatePostCommentReplyUseCase(commentId = parentCommentId, contents = contents, postId = postId, mentionList = mentionList).let { response -> when (response) { is NetworkResponse.Success -> { - _postCommentCreateSuccess.postValue(Event(true)) + _postCommentCreateState.postValue(Resource.success(true)) } else -> { - _postCommentCreateSuccess.postValue(Event(false)) + _postCommentCreateState.postValue(Resource.error("답글 작성 실패", false)) } } } From 0e623e539f0f39567a1464b884fbae451d8f33f8 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Tue, 9 Dec 2025 18:01:01 +0900 Subject: [PATCH 02/93] #706 [feature] Display snackbar when bottom sheet is visible --- .../dayo/presentation/screen/main/MainScreen.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/main/MainScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/main/MainScreen.kt index 6b6f7ed0e..1241bd57d 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/main/MainScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/main/MainScreen.kt @@ -112,10 +112,9 @@ internal fun MainScreen( SharedTransitionLayout { Scaffold( snackbarHost = { - SnackbarHost( - hostState = snackBarHostState, - modifier = Modifier.navigationBarsPadding() - ) + if (!bottomSheetController.isVisible) { + SnackbarHost(hostState = snackBarHostState) + } } ) { Box { @@ -295,7 +294,13 @@ internal fun MainScreen( sheetState = bottomSheetState, dragHandle = null ) { - bottomSheetController.sheetContent() + Box { + bottomSheetController.sheetContent() + SnackbarHost( + hostState = snackBarHostState, + modifier = Modifier.align(Alignment.BottomCenter) + ) + } } } } From 7270f16688c469112814200c14ee7befa6640b57 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Wed, 17 Dec 2025 20:44:44 +0900 Subject: [PATCH 03/93] [bug] Remove fixed button height to prevent text clipping with large fonts --- .../daily/dayo/presentation/screen/account/WithdrawScreen.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt index 657f1a90e..d9f8fa5fe 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -551,7 +552,7 @@ fun WithdrawHoldBottomSheet( label = stringResource(id = content.cancelButtonTextResId), modifier = Modifier .weight(1f) - .height(52.dp), + .defaultMinSize(minHeight = 52.dp), color = ButtonDefaults.buttonColors( containerColor = PrimaryL3_F2FBF7, contentColor = Primary_23C882 @@ -567,7 +568,7 @@ fun WithdrawHoldBottomSheet( label = stringResource(id = content.confirmButtonTextResId), modifier = Modifier .weight(1f) - .height(52.dp), + .defaultMinSize(minHeight = 52.dp), enabled = !(reason == WithdrawalReason.OTHER && otherReasonText.isBlank()), color = ButtonDefaults.buttonColors( containerColor = Primary_23C882, From b5d83a8f3533547aa2a5dd7265508bb43c362c1b Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Wed, 17 Dec 2025 20:45:10 +0900 Subject: [PATCH 04/93] [bug] Replace Row with FlowRow to support wrapping when text size increases --- .../screen/account/WithdrawScreen.kt | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt index d9f8fa5fe..b779099da 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio @@ -815,30 +816,36 @@ private fun WithdrawGuideContentUI( Spacer(modifier = Modifier.height(20.dp)) val guideStrings = words.ifEmpty { emptyList() } - Row( + FlowRow( modifier = Modifier .padding(bottom = 16.dp) - .wrapContentHeight() .fillMaxWidth(), horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, + verticalArrangement = Arrangement.Center ) { guideStrings.forEachIndexed { index, guide -> Text( text = guide, + modifier = Modifier.align(Alignment.CenterVertically), color = Gray1_50545B, textAlign = TextAlign.Center, - style = DayoTheme.typography.caption4 + style = DayoTheme.typography.caption4, ) if (index != guideStrings.lastIndex) { - Spacer(modifier = Modifier.width(6.dp)) + Spacer(modifier = Modifier + .width(6.dp) + .align(Alignment.CenterVertically)) Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_arrow_right), contentDescription = null, + modifier = Modifier + .size(12.dp) + .align(Alignment.CenterVertically), tint = Gray3_9FA5AE, - modifier = Modifier.size(12.dp) ) - Spacer(modifier = Modifier.width(6.dp)) + Spacer(modifier = Modifier + .width(6.dp) + .align(Alignment.CenterVertically)) } } } From b37b8db73221ae2a30fcd6902dff31358737d5d8 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Wed, 17 Dec 2025 20:52:20 +0900 Subject: [PATCH 05/93] [bug] Remove fixed button height to prevent text clipping with large fonts --- .../daily/dayo/presentation/screen/account/WithdrawScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt index b779099da..77250b29c 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt @@ -671,7 +671,7 @@ fun WithdrawButton( FilledRoundedCornerButton( modifier = Modifier .fillMaxWidth() - .height(52.dp), + .defaultMinSize(minHeight = 52.dp), label = stringResource(R.string.withdraw_confirm), color = ButtonDefaults.buttonColors( containerColor = Primary_23C882, From 15fa476776871c0c02a932007e39c100814c5cdf Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Thu, 19 Feb 2026 23:11:33 +0900 Subject: [PATCH 06/93] [chore] Add QA Issue Template --- .github/ISSUE_TEMPLATE/-qa--design-qa.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/-qa--design-qa.md diff --git a/.github/ISSUE_TEMPLATE/-qa--design-qa.md b/.github/ISSUE_TEMPLATE/-qa--design-qa.md new file mode 100644 index 000000000..15abc62b0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/-qa--design-qa.md @@ -0,0 +1,23 @@ +--- +name: "[QA] Design QA" +about: 디자인 QA 이슈를 등록합니다 +title: '[QA] Fix {Component} in {Screen}' +labels: QA +assignees: '' + +--- + +# 🎨 디자인 QA + +## 발견 위치 +- **화면**: +- **Figma 링크**: + +## 문제 설명 + + +### Annotation에 함께 등록된 properties +- 없음 + +## 상세 스펙 +- 없음 From d2d894b16c0d2b62e15022670f7c71764f8c20c5 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Thu, 19 Feb 2026 23:11:37 +0900 Subject: [PATCH 07/93] [chore] Add QA bot script (based on Gemini AI) --- .github/scripts/qa_bot.py | 319 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 .github/scripts/qa_bot.py diff --git a/.github/scripts/qa_bot.py b/.github/scripts/qa_bot.py new file mode 100644 index 000000000..6fa32e675 --- /dev/null +++ b/.github/scripts/qa_bot.py @@ -0,0 +1,319 @@ +#!/usr/bin/env python3 +import os +import re +import json +import subprocess +from pathlib import Path + +import google.generativeai as genai +from github import Github + + +SKIP_SCREEN_PREFIXES = {"main", "sub"} + +FONT_WEIGHT_MAP = { + 100: "FontWeight.Thin", + 200: "FontWeight.ExtraLight", + 300: "FontWeight.Light", + 400: "FontWeight.Normal", + 500: "FontWeight.Medium", + 600: "FontWeight.SemiBold", + 700: "FontWeight.Bold", + 800: "FontWeight.ExtraBold", + 900: "FontWeight.Black", +} + + +def fetch_issue(): + token = os.environ["GITHUB_TOKEN"] + repo_name = os.environ["REPO_FULL_NAME"] + issue_number = int(os.environ["ISSUE_NUMBER"]) + + g = Github(token) + repo = g.get_repo(repo_name) + issue = repo.get_issue(issue_number) + + return { + "number": issue_number, + "title": issue.title, + "body": issue.body or "", + } + + +def parse_issue(title, body): + result = {} + + title_match = re.search(r"\[QA\]\s+Fix\s+(\S+)\s+in\s+(\S+)", title) + if title_match: + result["component"] = title_match.group(1) + result["screen"] = title_match.group(2) + + screen_match = re.search(r"\*\*화면\*\*:\s*(.+)", body) + if screen_match: + result["screen"] = screen_match.group(1).strip() + + problem_match = re.search(r"## 문제 설명\n(.+?)(?:\n###|\n##)", body, re.DOTALL) + if problem_match: + result["problem"] = problem_match.group(1).strip() + + props_match = re.search( + r"### Annotation에 함께 등록된 properties\n(.+?)(?:\n## |\Z)", body, re.DOTALL + ) + if props_match: + raw = props_match.group(1).strip() + result["properties"] = raw if raw != "- 없음" else "" + + spec_match = re.search(r"## 상세 스펙\n(.+?)(?:\n##|\Z)", body, re.DOTALL) + if spec_match: + raw = spec_match.group(1).strip() + result["specs"] = raw if raw != "- 없음" else "" + + return result + + +def find_relevant_files(screen: str, component: str) -> list[Path]: + repo_root = Path(".") + presentation = repo_root / "presentation" + + parts = screen.split("_") + meaningful = [p for p in parts if p not in SKIP_SCREEN_PREFIXES] + + candidates: set[Path] = set() + + search_terms = [] + if meaningful: + search_terms.append("".join(p.capitalize() for p in meaningful)) + search_terms.append(meaningful[-1].capitalize()) + if len(meaningful) > 1: + search_terms.append(meaningful[0].capitalize()) + + if component: + search_terms.append(component) + + for term in search_terms: + for kt_file in presentation.rglob("*.kt"): + if term.lower() in kt_file.stem.lower(): + candidates.add(kt_file) + + result = subprocess.run( + ["grep", "-rl", "--include=*.kt", term, str(presentation)], + capture_output=True, + text=True, + ) + for line in result.stdout.strip().splitlines(): + if line: + candidates.add(Path(line)) + + def score(f: Path) -> int: + s = 0 + if "screen" in str(f): + s += 10 + for p in meaningful: + if p.lower() in f.stem.lower(): + s += 5 + return s + + return sorted(candidates, key=score, reverse=True)[:6] + + +def build_prompt(issue: dict, parsed: dict, files: list[Path]) -> str: + file_sections = [] + for f in files: + try: + content = f.read_text(encoding="utf-8") + if len(content) > 4000: + content = content[:4000] + "\n... (truncated)" + file_sections.append(f"### {f}\n```kotlin\n{content}\n```") + except Exception: + pass + + files_str = "\n\n".join(file_sections) if file_sections else "No relevant files found." + + color_hint = "" + props = parsed.get("properties", "") + if props: + r_match = re.search(r"r:\s*([\d.]+)", props) + g_match = re.search(r"g:\s*([\d.]+)", props) + b_match = re.search(r"b:\s*([\d.]+)", props) + if r_match and g_match and b_match: + r = int(float(r_match.group(1)) * 255) + g = int(float(g_match.group(1)) * 255) + b = int(float(b_match.group(1)) * 255) + hex_color = f"#{r:02X}{g:02X}{b:02X}" + color_hint = f"\n\n> Color hint: r/g/b values convert to hex **{hex_color}**" + + fw_hint = "" + fw_match = re.search(r"fontWeight[\"']?:\s*(\d+)", props) + if fw_match: + fw_val = int(fw_match.group(1)) + compose_fw = FONT_WEIGHT_MAP.get(fw_val, f"FontWeight({fw_val})") + fw_hint = f"\n\n> FontWeight hint: {fw_val} maps to **{compose_fw}** in Compose" + + return f"""You are an Android developer fixing a UI/design QA issue in a Kotlin Jetpack Compose Android app. + +## Issue +- Number: #{issue["number"]} +- Title: {issue["title"]} +- Screen: {parsed.get("screen", "unknown")} +- Component: {parsed.get("component", "unknown")} + +## Problem +{parsed.get("problem", "No description provided.")} + +## Design Properties +{parsed.get("properties", "none") or "none"}{color_hint}{fw_hint} + +## Detailed Specs +{parsed.get("specs", "none") or "none"} + +## Relevant Source Files +{files_str} + +## Rules +1. Make the smallest possible change that fixes the issue. +2. Only modify EXISTING files — never create new files. +3. The "original" field must be an exact verbatim substring of the file content. +4. Use existing color/style constants from the codebase if they match the required value. +5. For color fills: prefer the closest existing color constant over hardcoding a new hex. +6. For fontWeight: use the Compose FontWeight constant shown in the hint above. + +## Required Response Format +Respond with ONLY a JSON object — no markdown fences, no extra text: +{{ + "can_fix": true, + "explanation": "One-line explanation of what was changed", + "commit_message": "Imperative short commit message, no period", + "changes": [ + {{ + "file": "relative/path/to/File.kt", + "original": "exact verbatim code snippet to replace", + "replacement": "new code snippet" + }} + ] +}} + +If auto-fix is not safe or the issue requires logic changes beyond styling, respond with: +{{ + "can_fix": false, + "explanation": "Specific reason why this cannot be auto-fixed", + "changes": [] +}}""" + + +def call_gemini(prompt: str) -> dict: + genai.configure(api_key=os.environ["GEMINI_API_KEY"]) + model = genai.GenerativeModel("gemini-1.5-flash") + response = model.generate_content(prompt) + text = response.text.strip() + + json_fence = re.search(r"```(?:json)?\n(.+?)\n```", text, re.DOTALL) + if json_fence: + text = json_fence.group(1).strip() + + try: + return json.loads(text) + except json.JSONDecodeError: + return { + "can_fix": False, + "explanation": f"Failed to parse AI response: {text[:200]}", + "changes": [], + } + + +def apply_changes(changes: list[dict]) -> list[str]: + applied = [] + for change in changes: + path = Path(change["file"]) + if not path.exists(): + print(f"SKIP: file not found — {path}") + continue + + content = path.read_text(encoding="utf-8") + original = change["original"] + + if original not in content: + print(f"SKIP: original snippet not found in {path}") + print(f" Looking for: {original[:80]!r}") + continue + + new_content = content.replace(original, change["replacement"], 1) + path.write_text(new_content, encoding="utf-8") + applied.append(str(path)) + print(f"CHANGED: {path}") + + return applied + + +def write_outputs(can_fix: bool, commit_msg: str, explanation: str, pr_body: str): + Path("qa_bot_commit_msg.txt").write_text(commit_msg, encoding="utf-8") + Path("qa_bot_explanation.txt").write_text(explanation, encoding="utf-8") + Path("pr_body.md").write_text(pr_body, encoding="utf-8") + + output_file = os.environ.get("GITHUB_OUTPUT", "") + if output_file: + with open(output_file, "a") as f: + f.write(f"can_fix={'true' if can_fix else 'false'}\n") + + +def main(): + print("QA Bot starting...") + + issue = fetch_issue() + print(f"Issue #{issue['number']}: {issue['title']}") + + parsed = parse_issue(issue["title"], issue["body"]) + parsed["number"] = issue["number"] + print(f"Screen: {parsed.get('screen')} Component: {parsed.get('component')}") + + screen = parsed.get("screen", "") + component = parsed.get("component", "") + relevant_files = find_relevant_files(screen, component) + print(f"Found {len(relevant_files)} candidate file(s):") + for f in relevant_files: + print(f" {f}") + + prompt = build_prompt(issue, parsed, relevant_files) + print("Calling Gemini...") + result = call_gemini(prompt) + + can_fix = result.get("can_fix", False) + explanation = result.get("explanation", "No explanation provided.") + print(f"can_fix={can_fix} explanation={explanation}") + + if not can_fix or not result.get("changes"): + write_outputs( + can_fix=False, + commit_msg="", + explanation=explanation, + pr_body="", + ) + return + + changed = apply_changes(result["changes"]) + + if not changed: + write_outputs( + can_fix=False, + commit_msg="", + explanation="Code snippets could not be matched in any file.", + pr_body="", + ) + return + + commit_msg = result.get("commit_message", f"Fix {component} in {screen}") + pr_body = ( + f"Closes #{issue['number']}\n\n" + f"**Screen**: `{screen}` \n" + f"**Component**: `{component}`\n\n" + f"**Changes**: \n{explanation}\n\n" + f"**Files modified**: \n" + + "\n".join(f"- `{f}`" for f in changed) + + "\n\n> 🤖 Auto-generated by QA Bot" + ) + + write_outputs(can_fix=True, commit_msg=commit_msg, explanation=explanation, pr_body=pr_body) + print(f"Done — {len(changed)} file(s) modified.") + + +if __name__ == "__main__": + main() From eab8795358a7e02cea85b03d749e654225d18205 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Thu, 19 Feb 2026 23:11:42 +0900 Subject: [PATCH 08/93] [chore] Add QA bot GitHub Actions workflow --- .github/workflows/qa-bot.yml | 101 +++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 .github/workflows/qa-bot.yml diff --git a/.github/workflows/qa-bot.yml b/.github/workflows/qa-bot.yml new file mode 100644 index 000000000..457aac2ac --- /dev/null +++ b/.github/workflows/qa-bot.yml @@ -0,0 +1,101 @@ +name: QA Issue Auto-Fix Bot + +on: + issues: + types: [opened, labeled] + +permissions: + contents: write + pull-requests: write + issues: write + +jobs: + qa-fix: + # QA 라벨이 붙은 이슈가 열릴 때, 또는 기존 이슈에 QA 라벨이 붙을 때 실행 + if: | + (github.event.action == 'opened' && contains(github.event.issue.labels.*.name, 'QA')) || + (github.event.action == 'labeled' && github.event.label.name == 'QA') + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install Python dependencies + run: pip install google-generativeai PyGithub + + - name: Run QA Bot + id: qa_bot + env: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + REPO_FULL_NAME: ${{ github.repository }} + run: python .github/scripts/qa_bot.py + + - name: Create branch, commit and push + id: git_push + if: steps.qa_bot.outputs.can_fix == 'true' + run: | + git config user.name "qa-bot[bot]" + git config user.email "qa-bot[bot]@users.noreply.github.com" + + BRANCH="feature/issue-${{ github.event.issue.number }}" + + # 동일 브랜치가 이미 존재하면 삭제 (재실행 안전성) + git push origin --delete "$BRANCH" 2>/dev/null || true + + git checkout -b "$BRANCH" + git add -A + git commit -m "#${{ github.event.issue.number }} [bug] $(cat qa_bot_commit_msg.txt)" + git push origin "$BRANCH" + + echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" + + - name: Create Pull Request + id: create_pr + if: steps.qa_bot.outputs.can_fix == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_URL=$(gh pr create \ + --title "#${{ github.event.issue.number }} [bug] $(cat qa_bot_commit_msg.txt)" \ + --body-file pr_body.md \ + --base develop \ + --head "feature/issue-${{ github.event.issue.number }}") + + echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT" + + - name: Comment on issue — success + if: steps.qa_bot.outputs.can_fix == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_URL=$(cat pr_url.txt 2>/dev/null || echo "${{ steps.create_pr.outputs.pr_url }}") + gh issue comment ${{ github.event.issue.number }} \ + --body "🤖 **QA 봇**이 자동으로 수정 PR을 생성했습니다! + + PR: ${{ steps.create_pr.outputs.pr_url }} + + 수정 내용을 검토한 후 머지해주세요. 문제가 있으면 PR에 코멘트를 남겨주세요." + + - name: Comment on issue — cannot fix + if: steps.qa_bot.outputs.can_fix == 'false' || steps.qa_bot.outputs.can_fix == '' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + REASON=$(cat qa_bot_explanation.txt 2>/dev/null || echo "분석 결과 자동 수정이 어렵습니다.") + gh issue comment ${{ github.event.issue.number }} \ + --body "🤖 **QA 봇**이 이슈를 분석했지만 자동 수정이 어렵습니다. + + **사유**: $REASON + + 수동 검토가 필요합니다." From c6dd20d87bd05b08a4ff81b857c707db5eda2dd0 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Thu, 19 Feb 2026 23:11:47 +0900 Subject: [PATCH 09/93] [chore] Add QA bot configuration and usage documentation --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index 614c7356f..a05422bb9 100644 --- a/README.md +++ b/README.md @@ -67,5 +67,37 @@ git clone git@github.com:Daily-DAYO/DAYO_Android.git - shape drawable → border _(color) _ ([fill/line/fill_line]) _ (radius) e.g. border_white_fill_12 +## QA Auto-Fix Bot + +QA 라벨이 붙은 이슈가 생성되면 Gemini AI가 자동으로 코드를 수정하고 PR을 생성합니다. + +### Setup + +1. [Google AI Studio](https://aistudio.google.com/app/apikey)에서 Gemini API Key 발급 (무료) +2. GitHub 저장소 Settings → Secrets and variables → Actions → **New repository secret** + - Name: `GEMINI_API_KEY` + - Value: 발급받은 API Key +3. `.github/workflows/qa-bot.yml`이 `develop` 브랜치에 존재하면 자동 활성화 + +### How it works + +``` +QA 이슈 생성 (label: QA) + ↓ +GitHub Actions 트리거 + ↓ +이슈 파싱 → 화면명·컴포넌트·스펙 추출 + ↓ +관련 Kotlin 파일 탐색 + ↓ +Gemini 1.5 Flash 호출 → 수정 코드 생성 + ↓ +feature/issue-{N} 브랜치 생성 → 커밋 + ↓ +PR 자동 생성 + 이슈에 코멘트 +``` + +자동 수정이 어려운 이슈(로직 변경 필요 등)는 이슈에 사유를 코멘트합니다. + ## Copyright Copyrightⓒ 2021- DAYO, All rights reserved. From 447503123b85a02809e9d260e4aee669a9f48eeb Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Thu, 19 Feb 2026 23:19:58 +0900 Subject: [PATCH 10/93] [bug] Fix deprecated google-generativeai and PyGithub auth API --- .github/scripts/qa_bot.py | 14 ++++++++------ .github/workflows/qa-bot.yml | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/scripts/qa_bot.py b/.github/scripts/qa_bot.py index 6fa32e675..2f20102e3 100644 --- a/.github/scripts/qa_bot.py +++ b/.github/scripts/qa_bot.py @@ -5,8 +5,8 @@ import subprocess from pathlib import Path -import google.generativeai as genai -from github import Github +from google import genai +from github import Github, Auth SKIP_SCREEN_PREFIXES = {"main", "sub"} @@ -29,7 +29,7 @@ def fetch_issue(): repo_name = os.environ["REPO_FULL_NAME"] issue_number = int(os.environ["ISSUE_NUMBER"]) - g = Github(token) + g = Github(auth=Auth.Token(token)) repo = g.get_repo(repo_name) issue = repo.get_issue(issue_number) @@ -201,9 +201,11 @@ def build_prompt(issue: dict, parsed: dict, files: list[Path]) -> str: def call_gemini(prompt: str) -> dict: - genai.configure(api_key=os.environ["GEMINI_API_KEY"]) - model = genai.GenerativeModel("gemini-1.5-flash") - response = model.generate_content(prompt) + client = genai.Client(api_key=os.environ["GEMINI_API_KEY"]) + response = client.models.generate_content( + model="gemini-2.0-flash", + contents=prompt, + ) text = response.text.strip() json_fence = re.search(r"```(?:json)?\n(.+?)\n```", text, re.DOTALL) diff --git a/.github/workflows/qa-bot.yml b/.github/workflows/qa-bot.yml index 457aac2ac..2446fbead 100644 --- a/.github/workflows/qa-bot.yml +++ b/.github/workflows/qa-bot.yml @@ -30,7 +30,7 @@ jobs: python-version: '3.11' - name: Install Python dependencies - run: pip install google-generativeai PyGithub + run: pip install google-genai PyGithub - name: Run QA Bot id: qa_bot From de8f975e2a411438096ef6ff922c32bc085976a5 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Thu, 19 Feb 2026 23:27:06 +0900 Subject: [PATCH 11/93] [bug] Switch Gemini model to gemini-1.5-flash for free tier compatibility --- .github/scripts/qa_bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/qa_bot.py b/.github/scripts/qa_bot.py index 2f20102e3..ef8c92ad9 100644 --- a/.github/scripts/qa_bot.py +++ b/.github/scripts/qa_bot.py @@ -203,7 +203,7 @@ def build_prompt(issue: dict, parsed: dict, files: list[Path]) -> str: def call_gemini(prompt: str) -> dict: client = genai.Client(api_key=os.environ["GEMINI_API_KEY"]) response = client.models.generate_content( - model="gemini-2.0-flash", + model="gemini-1.5-flash", contents=prompt, ) text = response.text.strip() From 3a3d2d9e2960495cdb16c27d5e353fdaf7a4ae00 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Thu, 19 Feb 2026 23:31:37 +0900 Subject: [PATCH 12/93] [bug] Fix Gemini API version to v1 for gemini-1.5-flash compatibility --- .github/scripts/qa_bot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/scripts/qa_bot.py b/.github/scripts/qa_bot.py index ef8c92ad9..1657262b5 100644 --- a/.github/scripts/qa_bot.py +++ b/.github/scripts/qa_bot.py @@ -201,7 +201,10 @@ def build_prompt(issue: dict, parsed: dict, files: list[Path]) -> str: def call_gemini(prompt: str) -> dict: - client = genai.Client(api_key=os.environ["GEMINI_API_KEY"]) + client = genai.Client( + api_key=os.environ["GEMINI_API_KEY"], + http_options={"api_version": "v1"}, + ) response = client.models.generate_content( model="gemini-1.5-flash", contents=prompt, From bbce999df6c479aae179302bdc31cdb64e39d787 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Thu, 19 Feb 2026 23:55:38 +0900 Subject: [PATCH 13/93] [chore] Implement Gemini model fallback mechanism --- .github/scripts/qa_bot.py | 71 +++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/.github/scripts/qa_bot.py b/.github/scripts/qa_bot.py index 1657262b5..b2c62a13d 100644 --- a/.github/scripts/qa_bot.py +++ b/.github/scripts/qa_bot.py @@ -201,28 +201,55 @@ def build_prompt(issue: dict, parsed: dict, files: list[Path]) -> str: def call_gemini(prompt: str) -> dict: - client = genai.Client( - api_key=os.environ["GEMINI_API_KEY"], - http_options={"api_version": "v1"}, - ) - response = client.models.generate_content( - model="gemini-1.5-flash", - contents=prompt, - ) - text = response.text.strip() - - json_fence = re.search(r"```(?:json)?\n(.+?)\n```", text, re.DOTALL) - if json_fence: - text = json_fence.group(1).strip() - - try: - return json.loads(text) - except json.JSONDecodeError: - return { - "can_fix": False, - "explanation": f"Failed to parse AI response: {text[:200]}", - "changes": [], - } + models_to_try = [ + ("gemini-2.0-flash", "v1beta"), + ("gemini-2.0-flash-lite", "v1beta"), + ("gemini-1.5-flash", "v1beta"), + ("gemini-1.5-flash", "v1"), + ("gemini-1.5-flash-8b", "v1beta"), + ] + + last_error = None + + for model, version in models_to_try: + print(f"Trying Gemini model: {model} (version: {version or 'default'})") + try: + client_kwargs = {"api_key": os.environ["GEMINI_API_KEY"]} + if version: + client_kwargs["http_options"] = {"api_version": version} + + client = genai.Client(**client_kwargs) + + response = client.models.generate_content( + model=model, + contents=prompt, + ) + + text = response.text.strip() + + json_fence = re.search(r"```(?:json)?\n(.+?)\n```", text, re.DOTALL) + if json_fence: + text = json_fence.group(1).strip() + + try: + return json.loads(text) + except json.JSONDecodeError: + return { + "can_fix": False, + "explanation": f"Failed to parse AI response from {model}: {text[:200]}", + "changes": [], + } + + except Exception as e: + print(f" Failed with {model}: {e}") + last_error = e + continue + + return { + "can_fix": False, + "explanation": f"All Gemini models failed. Last error: {last_error}", + "changes": [], + } def apply_changes(changes: list[dict]) -> list[str]: From 142232c42c159a9b2e4cb1102f23b92d51a1bcf8 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Fri, 20 Feb 2026 00:16:26 +0900 Subject: [PATCH 14/93] [bug] Select Gemini model via runtime discovery --- .github/scripts/qa_bot.py | 195 ++++++++++++++++++++++++++++++++------ 1 file changed, 166 insertions(+), 29 deletions(-) diff --git a/.github/scripts/qa_bot.py b/.github/scripts/qa_bot.py index b2c62a13d..2472037bf 100644 --- a/.github/scripts/qa_bot.py +++ b/.github/scripts/qa_bot.py @@ -3,10 +3,11 @@ import re import json import subprocess +import time from pathlib import Path +from urllib import error, request + -from google import genai -from github import Github, Auth SKIP_SCREEN_PREFIXES = {"main", "sub"} @@ -25,6 +26,8 @@ def fetch_issue(): + from github import Auth, Github + token = os.environ["GITHUB_TOKEN"] repo_name = os.environ["REPO_FULL_NAME"] issue_number = int(os.environ["ISSUE_NUMBER"]) @@ -201,32 +204,146 @@ def build_prompt(issue: dict, parsed: dict, files: list[Path]) -> str: def call_gemini(prompt: str) -> dict: - models_to_try = [ - ("gemini-2.0-flash", "v1beta"), - ("gemini-2.0-flash-lite", "v1beta"), - ("gemini-1.5-flash", "v1beta"), - ("gemini-1.5-flash", "v1"), - ("gemini-1.5-flash-8b", "v1beta"), + api_key = os.environ.get("GEMINI_API_KEY", "").strip() + if not api_key: + return { + "can_fix": False, + "explanation": "Missing GEMINI_API_KEY", + "changes": [], + } + + def http_json(method: str, url: str, payload: dict | None = None) -> dict: + data = None + if payload is not None: + data = json.dumps(payload).encode("utf-8") + + req = request.Request( + url=url, + method=method, + data=data, + headers={ + "Content-Type": "application/json", + }, + ) + + try: + with request.urlopen(req, timeout=30) as resp: + body = resp.read().decode("utf-8") + return json.loads(body) if body else {} + except error.HTTPError as e: + raw = e.read().decode("utf-8", errors="replace") + try: + detail = json.loads(raw) if raw else {"error": {"code": e.code, "message": raw}} + except json.JSONDecodeError: + detail = {"error": {"code": e.code, "message": raw}} + raise RuntimeError(json.dumps(detail, ensure_ascii=True)) from e + + def list_models(api_version: str) -> list[dict]: + url = f"https://generativelanguage.googleapis.com/{api_version}/models?key={api_key}" + data = http_json("GET", url) + models = data.get("models") + return models if isinstance(models, list) else [] + + def extract_text(resp: dict) -> str: + candidates = resp.get("candidates") + if not isinstance(candidates, list) or not candidates: + return "" + content = candidates[0].get("content") if isinstance(candidates[0], dict) else None + parts = content.get("parts") if isinstance(content, dict) else None + if not isinstance(parts, list) or not parts: + return "" + texts: list[str] = [] + for p in parts: + if isinstance(p, dict) and isinstance(p.get("text"), str): + texts.append(p["text"]) + return "".join(texts).strip() + + preferred_tokens = [ + "gemini-2.0-flash-lite", + "gemini-2.0-flash", + "gemini-2.0", + "gemini-1.5-flash", + "gemini-1.5-pro", + "gemini-1.0-pro", + "gemini-pro", ] - last_error = None + attempts: list[tuple[str, str]] = [] + errors_seen: list[str] = [] + + for api_version in ("v1beta", "v1", "v1alpha"): + try: + models = list_models(api_version) + except Exception as e: + errors_seen.append(f"{api_version} list models failed: {e}") + continue + + generate_models: list[str] = [] + for m in models: + if not isinstance(m, dict): + continue + name = m.get("name") + methods = m.get("supportedGenerationMethods") + if not isinstance(name, str): + continue + if isinstance(methods, list) and "generateContent" in methods: + generate_models.append(name) + + if not generate_models: + errors_seen.append(f"{api_version}: no generateContent-capable models returned") + continue + + print(f"{api_version}: {len(generate_models)} generateContent-capable model(s)") + + ordered: list[str] = [] + for token in preferred_tokens: + for name in generate_models: + if token in name and name not in ordered: + ordered.append(name) + for name in generate_models: + if name not in ordered: + ordered.append(name) + + for model_name in ordered[:8]: + attempts.append((api_version, model_name)) + + if not attempts: + return { + "can_fix": False, + "explanation": "Unable to list any usable Gemini models. " + "; ".join(errors_seen)[:500], + "changes": [], + } + + last_err = "" + saw_limit_zero = False + saw_not_found = False + saw_other = False + for api_version, model_name in attempts: + print(f"Trying Gemini model: {model_name} (api: {api_version})") + url = f"https://generativelanguage.googleapis.com/{api_version}/{model_name}:generateContent?key={api_key}" + payload = { + "contents": [ + { + "role": "user", + "parts": [ + { + "text": prompt, + } + ], + } + ] + } - for model, version in models_to_try: - print(f"Trying Gemini model: {model} (version: {version or 'default'})") try: - client_kwargs = {"api_key": os.environ["GEMINI_API_KEY"]} - if version: - client_kwargs["http_options"] = {"api_version": version} - - client = genai.Client(**client_kwargs) - - response = client.models.generate_content( - model=model, - contents=prompt, - ) - - text = response.text.strip() - + resp = http_json("POST", url, payload) + text = extract_text(resp) + if not text: + return { + "can_fix": False, + "explanation": f"Gemini returned empty response for {model_name}", + "changes": [], + } + json_fence = re.search(r"```(?:json)?\n(.+?)\n```", text, re.DOTALL) if json_fence: text = json_fence.group(1).strip() @@ -236,18 +353,38 @@ def call_gemini(prompt: str) -> dict: except json.JSONDecodeError: return { "can_fix": False, - "explanation": f"Failed to parse AI response from {model}: {text[:200]}", + "explanation": f"Failed to parse AI response from {model_name}: {text[:200]}", "changes": [], } - except Exception as e: - print(f" Failed with {model}: {e}") - last_error = e + err_str = str(e) + last_err = err_str + if "RESOURCE_EXHAUSTED" in err_str and "limit\": 0" in err_str: + saw_limit_zero = True + continue + if "NOT_FOUND" in err_str: + saw_not_found = True + continue + saw_other = True + retry_delay = re.search(r"retryDelay\":\s*\"(\d+)s\"", err_str) + if retry_delay: + time.sleep(min(int(retry_delay.group(1)), 15)) + continue continue + if saw_limit_zero and not saw_other: + msg = "Gemini API free-tier quota appears to be 0 for all usable models in this project. Enable billing or use a different API key/project." + if saw_not_found: + msg += " Some model IDs were also not found." + return { + "can_fix": False, + "explanation": msg, + "changes": [], + } + return { "can_fix": False, - "explanation": f"All Gemini models failed. Last error: {last_error}", + "explanation": f"All Gemini models failed. Last error: {last_err}"[:500], "changes": [], } From 14e8b81935c805b3756e6c9aa26df2ce2c2d0eb2 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Fri, 20 Feb 2026 00:22:28 +0900 Subject: [PATCH 15/93] [bug] Include snippet-mismatch details when apply fails --- .github/scripts/qa_bot.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/scripts/qa_bot.py b/.github/scripts/qa_bot.py index 2472037bf..2fe7c730b 100644 --- a/.github/scripts/qa_bot.py +++ b/.github/scripts/qa_bot.py @@ -389,12 +389,14 @@ def extract_text(resp: dict) -> str: } -def apply_changes(changes: list[dict]) -> list[str]: - applied = [] +def apply_changes(changes: list[dict]) -> tuple[list[str], list[str]]: + applied: list[str] = [] + skipped: list[str] = [] for change in changes: path = Path(change["file"]) if not path.exists(): print(f"SKIP: file not found — {path}") + skipped.append(f"file not found: {path}") continue content = path.read_text(encoding="utf-8") @@ -403,6 +405,7 @@ def apply_changes(changes: list[dict]) -> list[str]: if original not in content: print(f"SKIP: original snippet not found in {path}") print(f" Looking for: {original[:80]!r}") + skipped.append(f"snippet not found in {path}: {original[:80]!r}") continue new_content = content.replace(original, change["replacement"], 1) @@ -410,7 +413,7 @@ def apply_changes(changes: list[dict]) -> list[str]: applied.append(str(path)) print(f"CHANGED: {path}") - return applied + return applied, skipped def write_outputs(can_fix: bool, commit_msg: str, explanation: str, pr_body: str): @@ -458,13 +461,16 @@ def main(): ) return - changed = apply_changes(result["changes"]) + changed, skipped = apply_changes(result["changes"]) if not changed: write_outputs( can_fix=False, commit_msg="", - explanation="Code snippets could not be matched in any file.", + explanation=( + "Code snippets could not be matched in any file. " + + "; ".join(skipped[:3]) + )[:500], pr_body="", ) return From d6309c81f1134c0d66a67d7e1a4531b9f0fbae63 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Fri, 20 Feb 2026 00:51:24 +0900 Subject: [PATCH 16/93] [bug] Apply QA bot edits via snippet_id catalog --- .github/scripts/qa_bot.py | 259 +++++++++++++++++++++++++++++++++++--- .gitignore | 8 +- 2 files changed, 250 insertions(+), 17 deletions(-) diff --git a/.github/scripts/qa_bot.py b/.github/scripts/qa_bot.py index 2fe7c730b..2f4e62d7f 100644 --- a/.github/scripts/qa_bot.py +++ b/.github/scripts/qa_bot.py @@ -12,6 +12,22 @@ SKIP_SCREEN_PREFIXES = {"main", "sub"} +ALLOWED_EDIT_PREFIXES = ( + "app/src/main/java/", + "data/src/main/java/", + "domain/src/main/java/", + "presentation/src/main/java/", +) + +DENIED_EDIT_CONTAINS = ( + "local.properties", + ".keystore", + ".jks", + "keystore", + "sentry.properties", + ".github/", +) + FONT_WEIGHT_MAP = { 100: "FontWeight.Thin", 200: "FontWeight.ExtraLight", @@ -25,6 +41,63 @@ } +def build_snippet_catalog(files: list[Path]) -> dict[str, dict[str, str]]: + def is_interesting(line: str) -> bool: + tokens = ( + "DayoTextField(", + "DayoPasswordTextField(", + "label =", + "placeholder =", + ".padding(", + "Modifier.", + "Text(", + "fontSize =", + "fontWeight =", + "color =", + "shape =", + "border =", + "keyboardOptions", + "keyboardActions", + ) + return any(t in line for t in tokens) + + catalog: dict[str, dict[str, str]] = {} + for file_idx, f in enumerate(files): + try: + content = f.read_text(encoding="utf-8") + except Exception: + continue + + snippets: dict[str, str] = {} + count = 0 + for line_idx, line in enumerate(content.splitlines(), start=1): + if not is_interesting(line): + continue + text = line.rstrip("\n") + if not text.strip(): + continue + + snippet_id = f"F{file_idx}S{count}L{line_idx}" + snippets[snippet_id] = text + count += 1 + if count >= 35: + break + + if snippets: + catalog[str(f)] = snippets + + return catalog + + +def is_allowed_edit_path(path: Path) -> bool: + s = path.as_posix().lstrip("./") + if any(x in s for x in DENIED_EDIT_CONTAINS): + return False + if not s.endswith(".kt"): + return False + return s.startswith(ALLOWED_EDIT_PREFIXES) + + def fetch_issue(): from github import Auth, Github @@ -119,18 +192,56 @@ def score(f: Path) -> int: return sorted(candidates, key=score, reverse=True)[:6] -def build_prompt(issue: dict, parsed: dict, files: list[Path]) -> str: +def build_prompt( + issue: dict, + parsed: dict, + files: list[Path], + snippet_catalog: dict[str, dict[str, str]], +) -> str: file_sections = [] + catalog_sections: list[str] = [] for f in files: try: content = f.read_text(encoding="utf-8") + + snippets = snippet_catalog.get(str(f), {}) + if snippets: + lines = [ + f"- [{sid}] {text}" + for sid, text in list(snippets.items())[:35] + if isinstance(text, str) + ] + catalog_sections.append( + "### " + + str(f) + + "\n" + + "Pick snippet_id(s) from this catalog:\n" + + "\n".join(lines) + ) + if len(content) > 4000: - content = content[:4000] + "\n... (truncated)" + pivot = content.find("DayoTextField(") + if pivot == -1: + pivot = content.find("DayoPasswordTextField(") + if pivot == -1: + pivot = content.find("@Composable") + + if pivot != -1: + start = max(pivot - 1800, 0) + end = min(start + 4000, len(content)) + content = content[start:end] + "\n... (truncated)" + else: + content = content[:4000] + "\n... (truncated)" file_sections.append(f"### {f}\n```kotlin\n{content}\n```") except Exception: pass files_str = "\n\n".join(file_sections) if file_sections else "No relevant files found." + catalog_str = ( + "\n\n".join(catalog_sections) + if catalog_sections + else "(No snippet catalog available.)" + ) color_hint = "" props = parsed.get("properties", "") @@ -172,13 +283,18 @@ def build_prompt(issue: dict, parsed: dict, files: list[Path]) -> str: ## Relevant Source Files {files_str} +## Snippet Catalog (Preferred) +To avoid mismatches, prefer returning snippet_id(s) from the catalog below instead of retyping "original": +{catalog_str} + ## Rules 1. Make the smallest possible change that fixes the issue. 2. Only modify EXISTING files — never create new files. -3. The "original" field must be an exact verbatim substring of the file content. + 3. Prefer snippet_id. If you use "original", it must be an exact verbatim substring of the file content. 4. Use existing color/style constants from the codebase if they match the required value. 5. For color fills: prefer the closest existing color constant over hardcoding a new hex. 6. For fontWeight: use the Compose FontWeight constant shown in the hint above. +7. Do not retype resource identifiers (e.g., R.string.*). Copy from the provided source or snippets. ## Required Response Format Respond with ONLY a JSON object — no markdown fences, no extra text: @@ -189,7 +305,8 @@ def build_prompt(issue: dict, parsed: dict, files: list[Path]) -> str: "changes": [ {{ "file": "relative/path/to/File.kt", - "original": "exact verbatim code snippet to replace", + "snippet_id": "F0S0L123", + "original": "(optional) exact verbatim code snippet to replace", "replacement": "new code snippet" }} ] @@ -389,18 +506,74 @@ def extract_text(resp: dict) -> str: } -def apply_changes(changes: list[dict]) -> tuple[list[str], list[str]]: +def apply_changes( + changes: list[dict], + snippet_catalog: dict[str, dict[str, str]], +) -> tuple[list[str], list[str]]: + repo_root = Path(".").resolve() applied: list[str] = [] skipped: list[str] = [] for change in changes: - path = Path(change["file"]) + file_str = change.get("file") if isinstance(change, dict) else None + if not isinstance(file_str, str) or not file_str.strip(): + skipped.append("missing file in change") + print("SKIP: missing file in change") + continue + + path = Path(file_str) + if path.is_absolute(): + skipped.append(f"absolute path not allowed: {path}") + print(f"SKIP: absolute path not allowed — {path}") + continue + + try: + resolved = path.resolve() + except Exception: + skipped.append(f"invalid path: {path}") + print(f"SKIP: invalid path — {path}") + continue + + try: + if not resolved.is_relative_to(repo_root): + skipped.append(f"path escapes repo: {path}") + print(f"SKIP: path escapes repo — {path}") + continue + except AttributeError: + if str(resolved).startswith(str(repo_root)) is False: + skipped.append(f"path escapes repo: {path}") + print(f"SKIP: path escapes repo — {path}") + continue + + if not is_allowed_edit_path(path): + skipped.append(f"path not allowed: {path}") + print(f"SKIP: path not allowed — {path}") + continue + if not path.exists(): print(f"SKIP: file not found — {path}") skipped.append(f"file not found: {path}") continue content = path.read_text(encoding="utf-8") - original = change["original"] + + original = "" + snippet_id = change.get("snippet_id") if isinstance(change, dict) else None + if isinstance(snippet_id, str) and snippet_id: + file_snips = snippet_catalog.get(str(path)) or snippet_catalog.get(str(path.as_posix())) + if isinstance(file_snips, dict) and isinstance(file_snips.get(snippet_id), str): + original = file_snips[snippet_id] + else: + skipped.append(f"snippet_id not found for {path}: {snippet_id}") + print(f"SKIP: snippet_id not found for {path} — {snippet_id}") + continue + else: + original = change.get("original", "") if isinstance(change, dict) else "" + + replacement = change.get("replacement", "") if isinstance(change, dict) else "" + if not isinstance(replacement, str) or not replacement: + skipped.append(f"missing replacement for {path}") + print(f"SKIP: missing replacement for {path}") + continue if original not in content: print(f"SKIP: original snippet not found in {path}") @@ -408,7 +581,33 @@ def apply_changes(changes: list[dict]) -> tuple[list[str], list[str]]: skipped.append(f"snippet not found in {path}: {original[:80]!r}") continue - new_content = content.replace(original, change["replacement"], 1) + replaced = False + line_no = None + if isinstance(snippet_id, str) and snippet_id: + m = re.search(r"L(\d+)$", snippet_id) + if m: + try: + line_no = int(m.group(1)) + except ValueError: + line_no = None + + if isinstance(line_no, int) and line_no >= 1: + lines = content.splitlines(True) + idx = line_no - 1 + if idx < len(lines): + current_line = lines[idx].rstrip("\r\n") + if current_line == original: + suffix = "" + if lines[idx].endswith("\r\n") and not replacement.endswith("\r\n"): + suffix = "\r\n" + elif lines[idx].endswith("\n") and not replacement.endswith("\n"): + suffix = "\n" + lines[idx] = replacement + suffix + new_content = "".join(lines) + replaced = True + + if not replaced: + new_content = content.replace(original, replacement, 1) path.write_text(new_content, encoding="utf-8") applied.append(str(path)) print(f"CHANGED: {path}") @@ -427,6 +626,24 @@ def write_outputs(can_fix: bool, commit_msg: str, explanation: str, pr_body: str f.write(f"can_fix={'true' if can_fix else 'false'}\n") +def build_repair_prompt( + issue: dict, + parsed: dict, + files: list[Path], + snippet_catalog: dict[str, dict[str, str]], + skipped: list[str], +) -> str: + base = build_prompt(issue, parsed, files, snippet_catalog) + details = "\n".join(f"- {s}" for s in skipped[:5]) if skipped else "- (none)" + return ( + base + + "\n\n## Apply Failure\n" + + "The previous JSON could not be applied because the exact 'original' snippet was not found.\n" + + "Fix this by returning snippet_id from the Snippet Catalog, or by copying 'original' from the provided files/snippets exactly.\n\n" + + details + ) + + def main(): print("QA Bot starting...") @@ -444,7 +661,8 @@ def main(): for f in relevant_files: print(f" {f}") - prompt = build_prompt(issue, parsed, relevant_files) + snippet_catalog = build_snippet_catalog(relevant_files) + prompt = build_prompt(issue, parsed, relevant_files, snippet_catalog) print("Calling Gemini...") result = call_gemini(prompt) @@ -461,16 +679,25 @@ def main(): ) return - changed, skipped = apply_changes(result["changes"]) - - if not changed: + changed, skipped = apply_changes(result["changes"], snippet_catalog) + if skipped: + repair_prompt = build_repair_prompt(issue, parsed, relevant_files, snippet_catalog, skipped) + print("Retrying Gemini for exact snippet match...") + repaired = call_gemini(repair_prompt) + if repaired.get("can_fix") and repaired.get("changes"): + changed2, skipped2 = apply_changes(repaired["changes"], snippet_catalog) + changed = list(dict.fromkeys(changed + changed2)) + skipped = skipped2 + + if not changed or skipped: + reason = ( + "Code snippets could not be matched in file content. " + + "; ".join(skipped[:3]) + )[:500] write_outputs( can_fix=False, commit_msg="", - explanation=( - "Code snippets could not be matched in any file. " - + "; ".join(skipped[:3]) - )[:500], + explanation=reason, pr_body="", ) return diff --git a/.gitignore b/.gitignore index 80750c2df..00ecf81cf 100644 --- a/.gitignore +++ b/.gitignore @@ -124,4 +124,10 @@ fabric.properties sentry.properties /*/sentry.properties -# End of https://www.toptal.com/developers/gitignore/api/androidstudio \ No newline at end of file +# Python +__pycache__/ +*.pyc +.venv*/ +venv*/ + +# End of https://www.toptal.com/developers/gitignore/api/androidstudio From de4c3bfc8e8aeb789f8835da29d9105d55bda662 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Fri, 20 Feb 2026 00:52:36 +0900 Subject: [PATCH 17/93] [chore] Make gradlew executable --- gradlew | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 gradlew diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 From d1b20e93f93c92ab23a45e26c0a617a5685559bc Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Fri, 20 Feb 2026 00:53:31 +0900 Subject: [PATCH 18/93] [bug] Improve QA bot relevant-file scoring --- .github/scripts/qa_bot.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/scripts/qa_bot.py b/.github/scripts/qa_bot.py index 2f4e62d7f..669588278 100644 --- a/.github/scripts/qa_bot.py +++ b/.github/scripts/qa_bot.py @@ -182,11 +182,24 @@ def find_relevant_files(screen: str, component: str) -> list[Path]: def score(f: Path) -> int: s = 0 - if "screen" in str(f): - s += 10 + p = f.as_posix().lower() + stem = f.stem.lower() + if "/screen/" in p: + s += 25 + if "screen" in stem: + s += 15 + if "screen" in p: + s += 5 + if "/view/" in p or stem.endswith("view"): + s += 4 + + if "/model/" in p or "model" in stem: + s -= 6 for p in meaningful: if p.lower() in f.stem.lower(): s += 5 + if p.lower() in f.as_posix().lower(): + s += 2 return s return sorted(candidates, key=score, reverse=True)[:6] From 5064907fbf769cc4f12c24448f9fefffd7d5154a Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Fri, 20 Feb 2026 01:30:08 +0900 Subject: [PATCH 19/93] [bug] Exclude QA bot artifacts from commits --- .github/scripts/qa_bot.py | 9 ++++++--- .github/workflows/qa-bot.yml | 15 ++++++++++----- .gitignore | 9 +++++++++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/.github/scripts/qa_bot.py b/.github/scripts/qa_bot.py index 669588278..b8482a26e 100644 --- a/.github/scripts/qa_bot.py +++ b/.github/scripts/qa_bot.py @@ -629,9 +629,12 @@ def apply_changes( def write_outputs(can_fix: bool, commit_msg: str, explanation: str, pr_body: str): - Path("qa_bot_commit_msg.txt").write_text(commit_msg, encoding="utf-8") - Path("qa_bot_explanation.txt").write_text(explanation, encoding="utf-8") - Path("pr_body.md").write_text(pr_body, encoding="utf-8") + out_dir = Path(".tmp_runner/qa-bot") + out_dir.mkdir(parents=True, exist_ok=True) + + (out_dir / "qa_bot_commit_msg.txt").write_text(commit_msg, encoding="utf-8") + (out_dir / "qa_bot_explanation.txt").write_text(explanation, encoding="utf-8") + (out_dir / "pr_body.md").write_text(pr_body, encoding="utf-8") output_file = os.environ.get("GITHUB_OUTPUT", "") if output_file: diff --git a/.github/workflows/qa-bot.yml b/.github/workflows/qa-bot.yml index 2446fbead..fe39fbeaa 100644 --- a/.github/workflows/qa-bot.yml +++ b/.github/workflows/qa-bot.yml @@ -54,8 +54,13 @@ jobs: git push origin --delete "$BRANCH" 2>/dev/null || true git checkout -b "$BRANCH" - git add -A - git commit -m "#${{ github.event.issue.number }} [bug] $(cat qa_bot_commit_msg.txt)" + git add -A -- . \ + ':(exclude)pr_body.md' \ + ':(exclude)qa_bot_commit_msg.txt' \ + ':(exclude)qa_bot_explanation.txt' \ + ':(exclude).tmp_runner' \ + ':(exclude).tmp_runner/**' + git commit -m "#${{ github.event.issue.number }} [bug] $(cat .tmp_runner/qa-bot/qa_bot_commit_msg.txt)" git push origin "$BRANCH" echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" @@ -67,8 +72,8 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | PR_URL=$(gh pr create \ - --title "#${{ github.event.issue.number }} [bug] $(cat qa_bot_commit_msg.txt)" \ - --body-file pr_body.md \ + --title "#${{ github.event.issue.number }} [bug] $(cat .tmp_runner/qa-bot/qa_bot_commit_msg.txt)" \ + --body-file .tmp_runner/qa-bot/pr_body.md \ --base develop \ --head "feature/issue-${{ github.event.issue.number }}") @@ -92,7 +97,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - REASON=$(cat qa_bot_explanation.txt 2>/dev/null || echo "분석 결과 자동 수정이 어렵습니다.") + REASON=$(cat .tmp_runner/qa-bot/qa_bot_explanation.txt 2>/dev/null || echo "분석 결과 자동 수정이 어렵습니다.") gh issue comment ${{ github.event.issue.number }} \ --body "🤖 **QA 봇**이 이슈를 분석했지만 자동 수정이 어렵습니다. diff --git a/.gitignore b/.gitignore index 00ecf81cf..5526fbb45 100644 --- a/.gitignore +++ b/.gitignore @@ -130,4 +130,13 @@ __pycache__/ .venv*/ venv*/ +# QA bot artifacts (should not be committed) +qa_bot_commit_msg.txt +qa_bot_explanation.txt +pr_body.md + +# Local QA bot debug artifacts +tmp_github_output.txt +.tmp_runner/ + # End of https://www.toptal.com/developers/gitignore/api/androidstudio From 00803b72bdbf6870b1bd80ec44a3bd9f86fc2a9e Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Fri, 20 Feb 2026 01:52:30 +0900 Subject: [PATCH 20/93] [bug] Fix qa-bot git add failure on ignored dir --- .github/workflows/qa-bot.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/qa-bot.yml b/.github/workflows/qa-bot.yml index fe39fbeaa..a0e636a72 100644 --- a/.github/workflows/qa-bot.yml +++ b/.github/workflows/qa-bot.yml @@ -54,12 +54,8 @@ jobs: git push origin --delete "$BRANCH" 2>/dev/null || true git checkout -b "$BRANCH" - git add -A -- . \ - ':(exclude)pr_body.md' \ - ':(exclude)qa_bot_commit_msg.txt' \ - ':(exclude)qa_bot_explanation.txt' \ - ':(exclude).tmp_runner' \ - ':(exclude).tmp_runner/**' + # NOTE: do not pass ignored paths as pathspecs; git add exits 1 when an ignored path is explicitly mentioned + git add -A git commit -m "#${{ github.event.issue.number }} [bug] $(cat .tmp_runner/qa-bot/qa_bot_commit_msg.txt)" git push origin "$BRANCH" From 0a148b1e65d79031d027fd283106203a262a5028 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Fri, 20 Feb 2026 17:49:24 +0900 Subject: [PATCH 21/93] [bug] Improve Gemini dynamic fallback with runtime model discovery --- .github/scripts/qa_bot.py | 190 ++++++++++++++++++++++++++++++-------- 1 file changed, 150 insertions(+), 40 deletions(-) diff --git a/.github/scripts/qa_bot.py b/.github/scripts/qa_bot.py index b8482a26e..9f525db2a 100644 --- a/.github/scripts/qa_bot.py +++ b/.github/scripts/qa_bot.py @@ -40,6 +40,111 @@ 900: "FontWeight.Black", } +def parse_gemini_error(err_str: str) -> dict: + if not err_str: + return {} + try: + parsed = json.loads(err_str) + if isinstance(parsed, dict): + return parsed + except json.JSONDecodeError: + pass + return {} + + +def extract_retry_delay_seconds(err_str: str, fallback: int = 0) -> int: + parsed = parse_gemini_error(err_str) + raw = parsed.get("retryDelay") if isinstance(parsed, dict) else None + if isinstance(raw, str): + m = re.fullmatch(r"(\d+)s", raw.strip()) + if m: + return int(m.group(1)) + + m = re.search(r'retryDelay\":\s*\"(\d+)s\"', err_str) + if m: + return int(m.group(1)) + + return fallback + + +def is_quota_exhausted_error(err_str: str) -> bool: + parsed = parse_gemini_error(err_str) + err = parsed.get("error") if isinstance(parsed, dict) else None + code = err.get("code") if isinstance(err, dict) else None + status = err.get("status") if isinstance(err, dict) else "" + message = err.get("message") if isinstance(err, dict) else "" + + details = err.get("details") if isinstance(err, dict) else None + reasons: list[str] = [] + if isinstance(details, list): + for d in details: + if isinstance(d, dict): + reason = d.get("reason") + if isinstance(reason, str): + reasons.append(reason) + + haystack = " ".join( + [ + str(status), + str(message), + " ".join(reasons), + err_str, + ] + ).lower() + + if code == 429: + return True + if "resource_exhausted" in haystack: + return True + if "rate_limit" in haystack: + return True + if "quota exceeded" in haystack: + return True + if "generativelanguage.googleapis.com/generate_content" in haystack: + return True + return False + + +def rank_model_metadata(model: dict) -> tuple[int, int, str]: + name = model.get("name") + display_name = model.get("displayName") + description = model.get("description") + + safe_name = name if isinstance(name, str) else "" + lower_name = safe_name.lower() + lower_display = display_name.lower() if isinstance(display_name, str) else "" + lower_description = description.lower() if isinstance(description, str) else "" + meta = " ".join([lower_name, lower_display, lower_description]) + + deprecated_penalty = 1 if "deprecated" in meta else 0 + preview_penalty = 1 if ("preview" in meta or "experimental" in meta or "-exp" in lower_name) else 0 + return (deprecated_penalty, preview_penalty, lower_name) + + +def order_candidate_models(models: list[dict]) -> list[str]: + candidate_models: list[dict] = [] + seen_names: set[str] = set() + + for m in models: + if not isinstance(m, dict): + continue + name = m.get("name") + methods = m.get("supportedGenerationMethods") + if not isinstance(name, str): + continue + if not name.startswith("models/gemini"): + continue + if not (isinstance(methods, list) and "generateContent" in methods): + continue + if name in seen_names: + continue + + seen_names.add(name) + candidate_models.append(m) + + candidate_models.sort(key=rank_model_metadata) + return [m["name"] for m in candidate_models if isinstance(m.get("name"), str)] + def build_snippet_catalog(files: list[Path]) -> dict[str, dict[str, str]]: def is_interesting(line: str) -> bool: @@ -369,10 +474,25 @@ def http_json(method: str, url: str, payload: dict | None = None) -> dict: raise RuntimeError(json.dumps(detail, ensure_ascii=True)) from e def list_models(api_version: str) -> list[dict]: - url = f"https://generativelanguage.googleapis.com/{api_version}/models?key={api_key}" - data = http_json("GET", url) - models = data.get("models") - return models if isinstance(models, list) else [] + all_models: list[dict] = [] + page_token = "" + + while True: + url = f"https://generativelanguage.googleapis.com/{api_version}/models?pageSize=100&key={api_key}" + if page_token: + url = f"{url}&pageToken={page_token}" + + data = http_json("GET", url) + models = data.get("models") + if isinstance(models, list): + all_models.extend(m for m in models if isinstance(m, dict)) + + next_token = data.get("nextPageToken") + if not isinstance(next_token, str) or not next_token: + break + page_token = next_token + + return all_models def extract_text(resp: dict) -> str: candidates = resp.get("candidates") @@ -388,18 +508,9 @@ def extract_text(resp: dict) -> str: texts.append(p["text"]) return "".join(texts).strip() - preferred_tokens = [ - "gemini-2.0-flash-lite", - "gemini-2.0-flash", - "gemini-2.0", - "gemini-1.5-flash", - "gemini-1.5-pro", - "gemini-1.0-pro", - "gemini-pro", - ] - attempts: list[tuple[str, str]] = [] errors_seen: list[str] = [] + seen_attempt_keys: set[str] = set() for api_version in ("v1beta", "v1", "v1alpha"): try: @@ -408,16 +519,7 @@ def extract_text(resp: dict) -> str: errors_seen.append(f"{api_version} list models failed: {e}") continue - generate_models: list[str] = [] - for m in models: - if not isinstance(m, dict): - continue - name = m.get("name") - methods = m.get("supportedGenerationMethods") - if not isinstance(name, str): - continue - if isinstance(methods, list) and "generateContent" in methods: - generate_models.append(name) + generate_models = order_candidate_models(models) if not generate_models: errors_seen.append(f"{api_version}: no generateContent-capable models returned") @@ -425,16 +527,11 @@ def extract_text(resp: dict) -> str: print(f"{api_version}: {len(generate_models)} generateContent-capable model(s)") - ordered: list[str] = [] - for token in preferred_tokens: - for name in generate_models: - if token in name and name not in ordered: - ordered.append(name) - for name in generate_models: - if name not in ordered: - ordered.append(name) - - for model_name in ordered[:8]: + for model_name in generate_models: + attempt_key = f"{api_version}:{model_name}" + if attempt_key in seen_attempt_keys: + continue + seen_attempt_keys.add(attempt_key) attempts.append((api_version, model_name)) if not attempts: @@ -445,6 +542,7 @@ def extract_text(resp: dict) -> str: } last_err = "" + saw_quota_error = False saw_limit_zero = False saw_not_found = False saw_other = False @@ -489,20 +587,25 @@ def extract_text(resp: dict) -> str: except Exception as e: err_str = str(e) last_err = err_str - if "RESOURCE_EXHAUSTED" in err_str and "limit\": 0" in err_str: - saw_limit_zero = True + if is_quota_exhausted_error(err_str): + saw_quota_error = True + if '"limit": 0' in err_str or "limit\\\": 0" in err_str: + saw_limit_zero = True + retry_seconds = extract_retry_delay_seconds(err_str) + if retry_seconds > 0: + time.sleep(min(retry_seconds, 15)) continue if "NOT_FOUND" in err_str: saw_not_found = True continue saw_other = True - retry_delay = re.search(r"retryDelay\":\s*\"(\d+)s\"", err_str) - if retry_delay: - time.sleep(min(int(retry_delay.group(1)), 15)) + retry_seconds = extract_retry_delay_seconds(err_str) + if retry_seconds > 0: + time.sleep(min(retry_seconds, 15)) continue continue - if saw_limit_zero and not saw_other: + if saw_limit_zero and saw_quota_error and not saw_other: msg = "Gemini API free-tier quota appears to be 0 for all usable models in this project. Enable billing or use a different API key/project." if saw_not_found: msg += " Some model IDs were also not found." @@ -512,6 +615,13 @@ def extract_text(resp: dict) -> str: "changes": [], } + if saw_quota_error and not saw_other: + return { + "can_fix": False, + "explanation": "All discovered Gemini models hit quota/rate limits. Consider increasing quota or switching API key/project.", + "changes": [], + } + return { "can_fix": False, "explanation": f"All Gemini models failed. Last error: {last_err}"[:500], From d6bd497d69239f11041dca607c50f30552843a4d Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Tue, 24 Feb 2026 22:05:40 +0900 Subject: [PATCH 22/93] #715 [feature] Fix Category Button --- .../screen/home/HomeDayoPickScreen.kt | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeDayoPickScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeDayoPickScreen.kt index 85dd45d6c..207965f25 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeDayoPickScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeDayoPickScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.grid.GridCells @@ -25,6 +26,7 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -181,21 +183,41 @@ private fun HomeDayoPickEmptyView() { } } +@OptIn(ExperimentalMaterialApi::class) @Composable fun CategoryButton( selectedCategory: String, onCategoryClick: () -> Unit ) { + Button( onClick = onCategoryClick, shape = RoundedCornerShape(8.dp), - contentPadding = PaddingValues(top = 6.dp, bottom = 6.dp, start = 12.dp, end = 4.dp), - colors = androidx.compose.material3.ButtonDefaults.buttonColors(containerColor = White_FFFFFF, contentColor = Gray2_767B83), - modifier = Modifier.border(1.dp, Gray6_F0F1F3, shape = RoundedCornerShape(8.dp)) + contentPadding = PaddingValues(start = 12.dp, end = 4.dp), + modifier = Modifier + .border(1.dp, Gray6_F0F1F3, shape = RoundedCornerShape(8.dp)) + .height(36.dp), + colors = ButtonDefaults.buttonColors( + containerColor = White_FFFFFF, + contentColor = Gray2_767B83 + ) ) { - Text(text = selectedCategory, style = DayoTheme.typography.caption3) - Spacer(modifier = Modifier.width(8.dp)) - Icon(Icons.Filled.ArrowDropDown, "category menu") + Row( + modifier = Modifier.height(36.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = selectedCategory, + style = DayoTheme.typography.caption3 + ) + Spacer(Modifier.width(8.dp)) + Icon( + imageVector = Icons.Filled.ArrowDropDown, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } } + } From f0d6313ce015213ddb70634897e61983d794c8b0 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Tue, 24 Feb 2026 23:08:18 +0900 Subject: [PATCH 23/93] #715 [feature] Fix BottomSheetDialog Ripple --- .../view/dialog/BottomSheetDialog.kt | 90 +++++++++++-------- 1 file changed, 55 insertions(+), 35 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt index 80340c231..156533d17 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt @@ -29,6 +29,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource @@ -115,42 +116,36 @@ fun BottomSheetDialog( buttons.forEachIndexed { index, button -> val interactionSource = remember { MutableInteractionSource() } val isPressed = interactionSource.collectIsPressedAsState().value - - Row( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .background( - if (isPressed) Gray6_F0F1F3 else White_FFFFFF, - RoundedCornerShape(12.dp, 12.dp, 0.dp, 0.dp) - ) - .padding(if (leftIconButtons == null) 16.dp else 12.dp) - .clickable( - onClick = button.second, - interactionSource = interactionSource, - indication = null - ), - horizontalArrangement = if (leftIconButtons == null) Arrangement.Center else Arrangement.SpaceBetween, - ) { - if (leftIconButtons != null && leftIconCheckedButtons != null) { - Icon( - imageVector = if (checkedButtonIndex == index) leftIconCheckedButtons[index] else leftIconButtons[index], - contentDescription = "", - modifier = Modifier.align(Alignment.CenterVertically), - tint = Color.Unspecified + if (leftIconButtons != null) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp, horizontal = 8.dp) + .clip(RoundedCornerShape(8.dp)) + .clickable( + onClick = button.second, + interactionSource = interactionSource + ) + .background(White_FFFFFF) + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + if (leftIconCheckedButtons != null) { + Icon( + imageVector = if (checkedButtonIndex == index) leftIconCheckedButtons[index] else leftIconButtons[index], + contentDescription = "", + modifier = Modifier.align(Alignment.CenterVertically), + tint = Color.Unspecified + ) + } + Text( + text = button.first, + modifier = Modifier.offset(8.dp, 0.dp), + color = if ((isFirstButtonColored && index == 0) || (checkedButtonIndex == index)) checkedColor else normalColor, + fontSize = 16.sp, + style = DayoTheme.typography.b4 ) - } - Text( - text = button.first, - modifier = Modifier.offset( - if (leftIconButtons == null) 0.dp else 8.dp, - 0.dp - ), - color = if ((isFirstButtonColored && index == 0) || (checkedButtonIndex == index)) checkedColor else normalColor, - fontSize = 16.sp, - style = DayoTheme.typography.b4 - ) - if (leftIconButtons != null) { Spacer(modifier = Modifier.weight(1f)) if (checkedButtonIndex == index) { Icon( @@ -161,7 +156,32 @@ fun BottomSheetDialog( ) } } + } else { + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background( + if (isPressed) Gray6_F0F1F3 else White_FFFFFF, + RoundedCornerShape(12.dp, 12.dp, 0.dp, 0.dp) + ) + .padding(16.dp) + .clickable( + onClick = button.second, + interactionSource = interactionSource, + indication = null + ), + horizontalArrangement = Arrangement.Center, + ) { + Text( + text = button.first, + color = if ((isFirstButtonColored && index == 0) || (checkedButtonIndex == index)) checkedColor else normalColor, + fontSize = 16.sp, + style = DayoTheme.typography.b4 + ) + } } + if (index < buttons.size - 1 && title.isEmpty()) { Divider( modifier = Modifier From 745710a5d8e7a0ad7ca1c442b3b4bf38453dd03a Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Wed, 11 Mar 2026 22:08:28 +0900 Subject: [PATCH 24/93] [QA] Show TextField label only when input is not empty --- .../presentation/screen/account/SignInEmailScreen.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/SignInEmailScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/SignInEmailScreen.kt index 4511ae24f..7b8390da9 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/SignInEmailScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/SignInEmailScreen.kt @@ -226,7 +226,11 @@ fun SignInEmailInputLayout( ) { DayoTextField( modifier = Modifier.focusRequester(focusRequesterEmail), - label = stringResource(R.string.sign_in_email_input_email_title), + label = if (emailValue.isNotEmpty()) { + stringResource(R.string.sign_in_email_input_email_title) + } else { + " " + }, placeholder = stringResource(R.string.sign_in_email_input_email_title), value = emailValue, onValueChange = onEmailChange, @@ -242,7 +246,11 @@ fun SignInEmailInputLayout( ) DayoPasswordTextField( modifier = Modifier.focusRequester(focusRequesterPassword), - label = stringResource(R.string.sign_in_email_input_password_title), + label = if (passwordValue.isNotEmpty()) { + stringResource(R.string.sign_in_email_input_password_title) + } else { + " " + }, placeholder = stringResource(R.string.sign_in_email_input_password_placeholder), value = passwordValue, onValueChange = onPasswordChange, From cb621ea3cb73fd6846cd37da08c5356b45fbfd56 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Wed, 11 Mar 2026 22:20:02 +0900 Subject: [PATCH 25/93] [QA] Prevent auth form buttons from being covered by the keyboard --- .../screen/account/ResetPasswordScreen.kt | 10 +++++++--- .../screen/account/SignInEmailScreen.kt | 14 +++++++------- .../screen/account/SignUpEmailScreen.kt | 10 +++++++--- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt index 35bc2c242..0b1320251 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt @@ -9,6 +9,8 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically import androidx.compose.animation.slideInVertically import androidx.compose.foundation.background +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -276,6 +278,8 @@ fun ResetPasswordScreen( isCheckingEmail: Boolean = false, setIsCheckingEmail: (Boolean) -> Unit = {}, ) { + val contentScrollState = rememberScrollState() + Surface( modifier = Modifier .background(DayoTheme.colorScheme.background) @@ -333,9 +337,10 @@ fun ResetPasswordScreen( Column( modifier = Modifier .background(DayoTheme.colorScheme.background) + .weight(1f) .padding(horizontal = 20.dp, vertical = 0.dp) .fillMaxWidth() - .wrapContentSize() + .verticalScroll(contentScrollState) ) { // Title 영역 Spacer(modifier = Modifier.height(8.dp)) @@ -432,7 +437,6 @@ fun ResetPasswordScreen( } } } - Spacer(modifier = Modifier.weight(1f)) ResetPasswordBottomLayout( resetPasswordStep = resetPasswordStep, onNextClick = { @@ -808,4 +812,4 @@ enum class ResetPasswordStep(val stepNum: Int) { EMAIL_VERIFICATION(2), // 인증번호 입력 NEW_PASSWORD_INPUT(3), // 비밀번호 입력 NEW_PASSWORD_CONFIRM(4), // 비밀번호 재입력 -} \ No newline at end of file +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/SignInEmailScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/SignInEmailScreen.kt index 7b8390da9..ba57c09d5 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/SignInEmailScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/SignInEmailScreen.kt @@ -3,6 +3,8 @@ package daily.dayo.presentation.screen.account import android.app.Activity import android.content.Intent import androidx.compose.foundation.background +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -17,7 +19,6 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -109,8 +110,7 @@ internal fun SignInEmailRoute( email = email, password = password ) - }, - accountViewModel = accountViewModel + } ) Loading( @@ -128,11 +128,11 @@ fun SignInEmailScreen( onBackClick: () -> Unit = {}, onForgetPasswordClick: () -> Unit = {}, onSignUpClick: () -> Unit = {}, - onSignInClick: (email: String, password: String) -> Unit = { _, _ -> }, - accountViewModel: AccountViewModel = hiltViewModel() + onSignInClick: (email: String, password: String) -> Unit = { _, _ -> } ) { val emailState = remember { mutableStateOf("") } val passwordState = remember { mutableStateOf("") } + val contentScrollState = rememberScrollState() val isSignInButtonEnabled = remember(emailState.value, passwordState.value) { emailState.value.isNotBlank() && passwordState.value.isNotBlank() } @@ -146,9 +146,10 @@ fun SignInEmailScreen( SignInEmailActionbarLayout(onBackClick = onBackClick) Column( modifier = Modifier + .weight(1f) .padding(horizontal = 20.dp, vertical = 0.dp) .fillMaxWidth() - .wrapContentSize(), + .verticalScroll(contentScrollState), verticalArrangement = Arrangement.Top ) { Spacer( @@ -171,7 +172,6 @@ fun SignInEmailScreen( onSignInClick = { onSignInClick(emailState.value, passwordState.value) } ) } - Spacer(modifier = Modifier.weight(1f)) SignInEmailBottomLayout( onSignUpClick = onSignUpClick, onSignInClick = { onSignInClick(emailState.value, passwordState.value) }, diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/SignUpEmailScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/SignUpEmailScreen.kt index c42bbbedc..8f6514aa0 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/SignUpEmailScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/SignUpEmailScreen.kt @@ -7,6 +7,8 @@ import android.graphics.BitmapFactory import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer @@ -564,6 +566,8 @@ fun SignUpEmailScaffold( onNextClick: () -> Unit = {}, content: @Composable (ColumnScope.() -> Unit), ) { + val contentScrollState = rememberScrollState() + BackHandler { onBackClick() } Scaffold( topBar = { @@ -604,9 +608,10 @@ fun SignUpEmailScaffold( Column( modifier = Modifier .background(DayoTheme.colorScheme.background) + .weight(1f) .padding(horizontal = 20.dp, vertical = 0.dp) .fillMaxWidth() - .wrapContentSize() + .verticalScroll(contentScrollState) ) { if (signUpStep.stepNum <= SignUpStep.PASSWORD_CONFIRM.stepNum) { SignUpEmailTitleLayout(title = title, subTitle = subTitle) @@ -615,7 +620,6 @@ fun SignUpEmailScaffold( } if (signUpStep != SignUpStep.SIGNUP_COMPLETE) { - Spacer(modifier = Modifier.weight(1f)) SignUpEmailBottomLayout( signUpStep = signUpStep, onNextClick = { onNextClick() }, @@ -683,4 +687,4 @@ fun SignUpEmailNextButton( enabled = isSignUpButtonEnabled, ) } -} \ No newline at end of file +} From a2ba13c5931ee69d7f53a29f01033748364e59d8 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Wed, 11 Mar 2026 22:20:18 +0900 Subject: [PATCH 26/93] [QA] Prevent change password button from being covered by the keyboard --- .../screen/settings/ChangePasswordScreen.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/settings/ChangePasswordScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/settings/ChangePasswordScreen.kt index ede8c5adc..3963f12c3 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/settings/ChangePasswordScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/settings/ChangePasswordScreen.kt @@ -3,6 +3,8 @@ package daily.dayo.presentation.screen.settings import android.content.Context import androidx.activity.compose.BackHandler import androidx.compose.foundation.background +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer @@ -12,7 +14,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text @@ -225,6 +226,8 @@ fun ChangePasswordScaffold( onNextClick: () -> Unit = {}, content: @Composable (ColumnScope.() -> Unit) = {}, ) { + val contentScrollState = rememberScrollState() + BackHandler { onBackClick() } Scaffold( topBar = { @@ -242,15 +245,15 @@ fun ChangePasswordScaffold( Column( modifier = Modifier .background(DayoTheme.colorScheme.background) + .weight(1f) .padding(horizontal = 18.dp, vertical = 0.dp) .fillMaxWidth() - .wrapContentSize() + .verticalScroll(contentScrollState) ) { ChangePasswordTitleLayout(title = title) content() } - Spacer(modifier = Modifier.weight(1f)) ChangePasswordBottomLayout( onNextClick = { onNextClick() }, isChangePasswordButtonEnabled = isNextButtonEnabled, @@ -341,4 +344,4 @@ enum class ChangePasswordStep(val stepNum: Int) { CUR_PASSWORD_INPUT(1), // 현재 비밀번호 입력 NEW_PASSWORD_INPUT(2), // 비밀번호 입력 NEW_PASSWORD_CONFIRM(3), // 비밀번호 재입력 -} \ No newline at end of file +} From cd98fcf581793c873ba59dd685c54ec51df466d9 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Wed, 11 Mar 2026 22:35:01 +0900 Subject: [PATCH 27/93] [QA] Keep login error snackbar visible above the keyboard --- .../dayo/presentation/screen/account/AccountScreen.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/AccountScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/AccountScreen.kt index 2f9f808ba..542c76e81 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/AccountScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/AccountScreen.kt @@ -3,6 +3,7 @@ package daily.dayo.presentation.screen.account import BottomSheetController import LocalBottomSheetController import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding @@ -35,7 +36,9 @@ internal fun AccountScreen( snackbarHost = { SnackbarHost( hostState = snackBarHostState, - modifier = Modifier.navigationBarsPadding() + modifier = Modifier + .navigationBarsPadding() + .imePadding() ) } ) { innerPadding -> @@ -75,4 +78,4 @@ internal fun AccountScreen( sealed class AccountScreen(val route: String) { object SignIn : AccountScreen(SignInRoute.route) -} \ No newline at end of file +} From ab755c3d2e4f599aa32de1e2b350649af1fd8351 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Wed, 11 Mar 2026 22:58:46 +0900 Subject: [PATCH 28/93] [QA] Prevent reset password email field error flicker --- .../screen/account/ResetPasswordScreen.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt index 35bc2c242..952e5c173 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt @@ -592,6 +592,14 @@ fun EmailInputLayout( requestEmailCertification: (String) -> Unit = {}, ) { val lastErrorMessage = remember { mutableStateOf("") } + val isEmailError = when { + email.isBlank() -> null + emailCertification == EmailCertificationState.INVALID_FORMAT || + emailCertification == EmailCertificationState.NOT_EXIST_EMAIL || + emailCertification == EmailCertificationState.OAUTH_EMAIL -> true + + else -> false + } LaunchedEffect(emailCertification) { lastErrorMessage.value = when (emailCertification) { @@ -612,9 +620,13 @@ fun EmailInputLayout( val formatValid = android.util.Patterns.EMAIL_ADDRESS.matcher(it).matches() setNextButtonEnabled(formatValid) setIsNextButtonClickable(formatValid) - if (!formatValid) { + if (it.isBlank()) { + setEmailCertification(EmailCertificationState.BEFORE_CERTIFICATION) + lastErrorMessage.value = "" + } else if (!formatValid) { setEmailCertification(EmailCertificationState.INVALID_FORMAT) } else { + setEmailCertification(EmailCertificationState.BEFORE_CERTIFICATION) lastErrorMessage.value = "" // INVALID FORMAT 에러 메시지가 다음 에러 메시지가 표시될 떄 남아 있지 않도록 value Clear } @@ -624,7 +636,7 @@ fun EmailInputLayout( trailingIconId = if (email.isNotBlank()) R.drawable.ic_trailing_check else null, errorTrailingIconId = R.drawable.ic_trailing_error, errorMessage = lastErrorMessage.value, - isError = if (email.isBlank()) null else !isNextButtonEnabled, + isError = isEmailError, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), ) } From db7fb4ec409871372e58808e4e37f4c44915eac9 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Wed, 11 Mar 2026 23:05:41 +0900 Subject: [PATCH 29/93] [QA] Show TextField label only when input is not empty --- .../dayo/presentation/screen/account/ResetPasswordScreen.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt index 952e5c173..cc18e1091 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt @@ -631,7 +631,11 @@ fun EmailInputLayout( // INVALID FORMAT 에러 메시지가 다음 에러 메시지가 표시될 떄 남아 있지 않도록 value Clear } }, - label = stringResource(R.string.email), + label = if (email.isNotEmpty()) { + stringResource(R.string.email) + } else { + " " + }, placeholder = stringResource(R.string.reset_password_email_placeholder), trailingIconId = if (email.isNotBlank()) R.drawable.ic_trailing_check else null, errorTrailingIconId = R.drawable.ic_trailing_error, From c9de0552e48fbf5b715ec5c493f4d831ad40c087 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Wed, 11 Mar 2026 23:12:49 +0900 Subject: [PATCH 30/93] [QA] Show TextField label only when input is not empty --- .../daily/dayo/presentation/screen/account/SetEmailView.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailView.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailView.kt index 1c22c4714..28b35e108 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailView.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailView.kt @@ -54,7 +54,11 @@ fun SetEmailView( requestEmailCertification(it) } }, - label = stringResource(R.string.email), + label = if (email.isNotEmpty()) { + stringResource(R.string.email) + } else { + " " + }, placeholder = stringResource(R.string.sign_up_email_set_address_placeholder), trailingIconId = if (email.isNotBlank()) R.drawable.ic_trailing_check else null, errorTrailingIconId = R.drawable.ic_trailing_error, From efa5d6dc3b5f49705bbabcf474e49a28228f1b37 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Wed, 11 Mar 2026 23:23:48 +0900 Subject: [PATCH 31/93] [QA] Prevent duplicate top inset on sign-up navigation --- .../dayo/presentation/screen/account/SignUpEmailScreen.kt | 4 +++- .../java/daily/dayo/presentation/view/TopNavigation.kt | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/SignUpEmailScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/SignUpEmailScreen.kt index c42bbbedc..ffc013724 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/SignUpEmailScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/SignUpEmailScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -592,6 +593,7 @@ fun SignUpEmailScaffold( ) } }, + windowInsets = WindowInsets(0, 0, 0, 0), ) } ) { innerPadding -> @@ -683,4 +685,4 @@ fun SignUpEmailNextButton( enabled = isSignUpButtonEnabled, ) } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/daily/dayo/presentation/view/TopNavigation.kt b/presentation/src/main/java/daily/dayo/presentation/view/TopNavigation.kt index 00b4c24de..1e5c56ac9 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/TopNavigation.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/TopNavigation.kt @@ -1,6 +1,7 @@ package daily.dayo.presentation.view import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.CenterAlignedTopAppBar @@ -25,7 +26,8 @@ fun TopNavigation( title: String = "", leftIcon: @Composable () -> Unit = {}, rightIcon: @Composable () -> Unit = {}, - titleAlignment: TopNavigationAlign = TopNavigationAlign.LEFT + titleAlignment: TopNavigationAlign = TopNavigationAlign.LEFT, + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets ) { when (titleAlignment) { TopNavigationAlign.LEFT -> { @@ -34,6 +36,7 @@ fun TopNavigation( containerColor = White_FFFFFF, titleContentColor = Dark, ), + windowInsets = windowInsets, navigationIcon = leftIcon, actions = { rightIcon() }, title = { @@ -48,6 +51,7 @@ fun TopNavigation( containerColor = White_FFFFFF, titleContentColor = Dark, ), + windowInsets = windowInsets, navigationIcon = leftIcon, actions = { rightIcon() }, title = { @@ -93,4 +97,4 @@ fun PreviewTopNavigation() { titleAlignment = TopNavigationAlign.CENTER ) } -} \ No newline at end of file +} From 0044be2a71ab71b36c3aea9ee54a8855a17904b6 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Wed, 11 Mar 2026 23:38:13 +0900 Subject: [PATCH 32/93] [QA] Reset email verification state on resend --- .../dayo/presentation/screen/account/ResetPasswordScreen.kt | 6 +++++- .../screen/account/SetEmailCertificationView.kt | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt index 35bc2c242..cd45f8521 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt @@ -712,6 +712,10 @@ private fun EmailCertificationLayout( text = stringResource(R.string.reset_password_email_certification_resend_button), onClick = { tryCount++ + setCertificationInputCode("") + timerErrorMessageRedId.value = + R.string.reset_password_email_certification_fail_wrong + setIsEmailCertificateError(false) requestEmailCertification(email) }, underline = true, @@ -808,4 +812,4 @@ enum class ResetPasswordStep(val stepNum: Int) { EMAIL_VERIFICATION(2), // 인증번호 입력 NEW_PASSWORD_INPUT(3), // 비밀번호 입력 NEW_PASSWORD_CONFIRM(4), // 비밀번호 재입력 -} \ No newline at end of file +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailCertificationView.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailCertificationView.kt index a66b5bbc5..6c99dcf3a 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailCertificationView.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailCertificationView.kt @@ -117,7 +117,11 @@ fun SetEmailCertificationView( text = stringResource(R.string.sign_up_email_set_address_resend_button), onClick = { tryCount++ + setCertificationInputCode("") isTimeOut.value = false + timerErrorMessageRedId.value = + R.string.sign_up_email_set_address_certification_fail_wrong + setIsEmailCertificateError(false) requestEmailCertification(email) }, underline = true, @@ -125,4 +129,4 @@ fun SetEmailCertificationView( ) } } -} \ No newline at end of file +} From 794a78e3c2b4a89ad7fc6c72a895ba3468cace7a Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Wed, 11 Mar 2026 23:48:17 +0900 Subject: [PATCH 33/93] [QA] Gate email verification until code is ready --- .../screen/account/ResetPasswordScreen.kt | 9 +++++++-- .../screen/account/SetEmailCertificationView.kt | 11 +++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt index cd45f8521..a11b032c6 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt @@ -647,6 +647,9 @@ private fun EmailCertificationLayout( requestEmailCertification: (String) -> Unit = {}, ) { val certificateEmailAuthCodeFormat = Regex("^\\d{6}$") + val isServerCertificationCodeReady = + certificationCode != EMAIL_CERTIFICATE_AUTH_CODE_INITIAL.toString() && + certificationCode != RESET_PASSWORD_EMAIL_CERTIFICATE_AUTH_CODE_FAIL.toString() var tryCount by remember { mutableStateOf(1) } val isPaused = remember { mutableStateOf(false) } @@ -657,10 +660,12 @@ private fun EmailCertificationLayout( remember { mutableStateOf((R.string.reset_password_email_certification_fail_wrong)) } setNextButtonEnabled( - certificateEmailAuthCodeFormat.matches(certificationInputCode) + certificateEmailAuthCodeFormat.matches(certificationInputCode) && + isServerCertificationCodeReady ) setIsNextButtonClickable( - certificateEmailAuthCodeFormat.matches(certificationInputCode) + certificateEmailAuthCodeFormat.matches(certificationInputCode) && + isServerCertificationCodeReady ) key(tryCount) { diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailCertificationView.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailCertificationView.kt index 6c99dcf3a..e8d0825f9 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailCertificationView.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailCertificationView.kt @@ -48,6 +48,9 @@ fun SetEmailCertificationView( requestEmailCertification: (String) -> Unit = {}, ) { val certificateEmailAuthCodeFormat = Regex("^\\d{6}$") + val isServerCertificationCodeReady = + certificationCode != AccountViewModel.EMAIL_CERTIFICATE_AUTH_CODE_INITIAL.toString() && + certificationCode != AccountViewModel.SIGN_UP_EMAIL_CERTIFICATE_AUTH_CODE_FAIL.toString() var tryCount by remember { mutableStateOf(1) } val isPaused = remember { mutableStateOf(false) } @@ -57,10 +60,14 @@ fun SetEmailCertificationView( val isTimeOut = remember { mutableStateOf(false) } setNextButtonEnabled( - certificateEmailAuthCodeFormat.matches(certificationInputCode) && !isTimeOut.value + certificateEmailAuthCodeFormat.matches(certificationInputCode) && + isServerCertificationCodeReady && + !isTimeOut.value ) setIsNextButtonClickable( - certificateEmailAuthCodeFormat.matches(certificationInputCode) && !isTimeOut.value + certificateEmailAuthCodeFormat.matches(certificationInputCode) && + isServerCertificationCodeReady && + !isTimeOut.value ) key(tryCount) { From 0ec46ae77893fd6b45977b019cb8ca06fd525fd5 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Wed, 11 Mar 2026 23:53:35 +0900 Subject: [PATCH 34/93] [QA] Restore 4dp spacing in auth subtitles --- .../screen/account/ResetPasswordScreen.kt | 28 ++++++++++--------- .../screen/account/SignUpEmailScreen.kt | 20 +++++++------ 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt index a11b032c6..57254f18e 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt @@ -354,19 +354,21 @@ fun ResetPasswordScreen( AnimatedVisibility( visible = (resetPasswordStep.stepNum in 1..2), ) { - Spacer( - modifier = Modifier - .fillMaxWidth() - .height(4.dp) - ) - Text( - text = if (resetPasswordStep == ResetPasswordStep.EMAIL_INPUT) - stringResource(R.string.reset_password_email_sub_title) - else if (resetPasswordStep == ResetPasswordStep.EMAIL_VERIFICATION) - stringResource(R.string.reset_password_new_password_sub_title) - else "", - style = DayoTheme.typography.b6.copy(color = Gray2_767B83), - ) + Column { + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(4.dp) + ) + Text( + text = if (resetPasswordStep == ResetPasswordStep.EMAIL_INPUT) + stringResource(R.string.reset_password_email_sub_title) + else if (resetPasswordStep == ResetPasswordStep.EMAIL_VERIFICATION) + stringResource(R.string.reset_password_new_password_sub_title) + else "", + style = DayoTheme.typography.b6.copy(color = Gray2_767B83), + ) + } } // Contents 영역 diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/SignUpEmailScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/SignUpEmailScreen.kt index ffc013724..8cef0fa6f 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/SignUpEmailScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/SignUpEmailScreen.kt @@ -263,15 +263,17 @@ fun SignUpEmailTitleLayout( // SubTitle 영역 AnimatedVisibility(visible = subTitle.isNotBlank()) { - Spacer( - modifier = Modifier - .fillMaxWidth() - .height(4.dp) - ) - Text( - text = subTitle, - style = DayoTheme.typography.b6.copy(color = Gray2_767B83), - ) + Column { + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(4.dp) + ) + Text( + text = subTitle, + style = DayoTheme.typography.b6.copy(color = Gray2_767B83), + ) + } } } From cdc3bf47fdc65e6ca82dd1798507b0b5e8e61600 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Thu, 12 Mar 2026 00:00:41 +0900 Subject: [PATCH 35/93] [QA] Update verification label color token --- .../dayo/presentation/screen/account/ResetPasswordScreen.kt | 2 ++ .../presentation/screen/account/SetEmailCertificationView.kt | 2 ++ .../src/main/java/daily/dayo/presentation/view/TextField.kt | 5 +++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt index 57254f18e..8c770ad24 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt @@ -53,6 +53,7 @@ import daily.dayo.presentation.screen.account.model.EmailExistenceStatus import daily.dayo.presentation.theme.Dark import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray3_9FA5AE import daily.dayo.presentation.theme.White_FFFFFF import daily.dayo.presentation.view.DayoPasswordTextField import daily.dayo.presentation.view.DayoTextButton @@ -685,6 +686,7 @@ private fun EmailCertificationLayout( isError = isEmailCertificateError ?: false, errorMessage = stringResource(timerErrorMessageRedId.value), timeOutErrorMessage = stringResource(R.string.reset_password_email_certification_fail_time_out), + labelColor = Gray3_9FA5AE, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), ) } diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailCertificationView.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailCertificationView.kt index e8d0825f9..be0a2fe6e 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailCertificationView.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailCertificationView.kt @@ -25,6 +25,7 @@ import daily.dayo.presentation.screen.account.model.EmailCertificationState import daily.dayo.presentation.screen.account.model.SignUpStep import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray3_9FA5AE import daily.dayo.presentation.view.DayoTextButton import daily.dayo.presentation.view.DayoTimerTextField import daily.dayo.presentation.viewmodel.AccountViewModel @@ -85,6 +86,7 @@ fun SetEmailCertificationView( isError = isEmailCertificateError ?: false, errorMessage = stringResource(timerErrorMessageRedId.value), timeOutErrorMessage = stringResource(R.string.sign_up_email_set_address_certification_fail_time_out), + labelColor = Gray3_9FA5AE, onTimeOut = { isTimeOut.value = true setNextButtonEnabled(false) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/TextField.kt b/presentation/src/main/java/daily/dayo/presentation/view/TextField.kt index c3fbfd17a..f7516f0a3 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/TextField.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/TextField.kt @@ -323,6 +323,7 @@ fun DayoTimerTextField( isError: Boolean = false, errorMessage: String = "", timeOutErrorMessage: String = stringResource(id = R.string.email_address_certificate_alert_message_time_fail), + labelColor: Color = Gray4_C5CAD2, onTimeOut: (() -> Unit) = { }, textAlign: TextAlign = TextAlign.Left, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, @@ -351,7 +352,7 @@ fun DayoTimerTextField( Text( text = label, style = DayoTheme.typography.caption3.copy( - color = Gray4_C5CAD2, + color = labelColor, fontWeight = FontWeight.SemiBold ) ) @@ -528,4 +529,4 @@ private fun PreviewOutlinedTextField() { placeholder = stringResource(id = R.string.report_post_reason_other_hint), maxLength = 5 ) -} \ No newline at end of file +} From 383897884d51c34ca6099953895836b7f22a87ea Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Thu, 12 Mar 2026 01:05:36 +0900 Subject: [PATCH 36/93] [QA] Show clear and visibility icons in shared password field --- .../view/DayoPasswordTextFieldTest.kt | 163 ++++++++++++++++++ .../daily/dayo/presentation/view/TextField.kt | 54 ++++-- 2 files changed, 206 insertions(+), 11 deletions(-) create mode 100644 presentation/src/androidTest/java/daily/dayo/presentation/view/DayoPasswordTextFieldTest.kt diff --git a/presentation/src/androidTest/java/daily/dayo/presentation/view/DayoPasswordTextFieldTest.kt b/presentation/src/androidTest/java/daily/dayo/presentation/view/DayoPasswordTextFieldTest.kt new file mode 100644 index 000000000..04bfe2c19 --- /dev/null +++ b/presentation/src/androidTest/java/daily/dayo/presentation/view/DayoPasswordTextFieldTest.kt @@ -0,0 +1,163 @@ +package daily.dayo.presentation.view + +import androidx.activity.ComponentActivity +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onAllNodesWithContentDescription +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import daily.dayo.presentation.theme.DayoTheme +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DayoPasswordTextFieldTest { + + @get:Rule + val composeRule = createAndroidComposeRule() + + private fun assertContentDescriptionCount(contentDescription: String, expectedCount: Int) { + composeRule.onAllNodesWithContentDescription(contentDescription) + .assertCountEquals(expectedCount) + } + + @Test + fun givenTextAndDefaultFlags_whenRendered_thenClearAndEyeIconsAreBothShown() { + var passwordValue by mutableStateOf("password") + + composeRule.setContent { + DayoTheme { + DayoPasswordTextField( + value = passwordValue, + onValueChange = { passwordValue = it } + ) + } + } + + assertContentDescriptionCount("Clear password", 1) + assertContentDescriptionCount("Show password", 1) + } + + @Test + fun givenText_whenClearIconTapped_thenFieldIsEmptied() { + var passwordValue by mutableStateOf("password") + + composeRule.setContent { + DayoTheme { + DayoPasswordTextField( + value = passwordValue, + onValueChange = { passwordValue = it } + ) + } + } + + composeRule.onNodeWithContentDescription("Clear password").performClick() + + composeRule.runOnIdle { + assertEquals("", passwordValue) + } + assertContentDescriptionCount("Clear password", 0) + assertContentDescriptionCount("Show password", 1) + } + + @Test + fun givenDefaultVisibilityIcon_whenTapped_thenContentDescriptionChangesToHidePassword() { + var passwordValue by mutableStateOf("password") + + composeRule.setContent { + DayoTheme { + DayoPasswordTextField( + value = passwordValue, + onValueChange = { passwordValue = it } + ) + } + } + + composeRule.onNodeWithContentDescription("Show password").performClick() + + assertContentDescriptionCount("Hide password", 1) + } + + @Test + fun givenErrorState_whenRendered_thenOnlyErrorIconIsShown() { + var passwordValue by mutableStateOf("password") + + composeRule.setContent { + DayoTheme { + DayoPasswordTextField( + value = passwordValue, + onValueChange = { passwordValue = it }, + isError = true, + errorMessage = "error" + ) + } + } + + assertContentDescriptionCount("error icon", 1) + assertContentDescriptionCount("Clear password", 0) + assertContentDescriptionCount("Show password", 0) + } + + @Test + fun givenVisibilityIconHiddenAndTextExists_whenRendered_thenOnlyClearIconIsShown() { + var passwordValue by mutableStateOf("password") + + composeRule.setContent { + DayoTheme { + DayoPasswordTextField( + value = passwordValue, + onValueChange = { passwordValue = it }, + showVisibilityIcon = false + ) + } + } + + assertContentDescriptionCount("Clear password", 1) + assertContentDescriptionCount("Show password", 0) + assertContentDescriptionCount("Hide password", 0) + } + + @Test + fun givenVisibilityIconHiddenAndTextBlank_whenRendered_thenNoTrailingIconsAreShown() { + var passwordValue by mutableStateOf("") + + composeRule.setContent { + DayoTheme { + DayoPasswordTextField( + value = passwordValue, + onValueChange = { passwordValue = it }, + showVisibilityIcon = false + ) + } + } + + assertContentDescriptionCount("Clear password", 0) + assertContentDescriptionCount("Show password", 0) + assertContentDescriptionCount("Hide password", 0) + assertContentDescriptionCount("error icon", 0) + } + + @Test + fun givenDisabledFieldWithText_whenRendered_thenClearIsHiddenAndEyeRemains() { + var passwordValue by mutableStateOf("password") + + composeRule.setContent { + DayoTheme { + DayoPasswordTextField( + value = passwordValue, + onValueChange = { passwordValue = it }, + isEnabled = false + ) + } + } + + assertContentDescriptionCount("Clear password", 0) + assertContentDescriptionCount("Show password", 1) + } +} diff --git a/presentation/src/main/java/daily/dayo/presentation/view/TextField.kt b/presentation/src/main/java/daily/dayo/presentation/view/TextField.kt index f7516f0a3..3356c721e 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/TextField.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/TextField.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxWidth @@ -198,6 +199,8 @@ fun DayoPasswordTextField( textAlign: TextAlign = TextAlign.Left, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, onErrorIconClick: (() -> Unit) = { }, + showClearIcon: Boolean = true, + showVisibilityIcon: Boolean = true, keyboardOptions: KeyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), keyboardActions: KeyboardActions = KeyboardActions.Default, isEnabled: Boolean = true, @@ -207,6 +210,15 @@ fun DayoPasswordTextField( verticalArrangement = Arrangement.spacedBy(4.dp) ) { var passwordHidden by remember { mutableStateOf(true) } + val showError = isError == true + val shouldShowClear = showClearIcon && isEnabled && value.isNotBlank() && !showError + val shouldShowVisibility = showVisibilityIcon && !showError + val trailingContentWidth = when { + showError -> 20.dp + shouldShowClear && shouldShowVisibility -> 48.dp + shouldShowClear || shouldShowVisibility -> 20.dp + else -> 0.dp + } if (label.isNotEmpty()) { Text( @@ -254,7 +266,7 @@ fun DayoPasswordTextField( contentPadding = PaddingValues( start = contentPadding.calculateStartPadding(LayoutDirection.Ltr), top = contentPadding.calculateTopPadding(), - end = contentPadding.calculateEndPadding(LayoutDirection.Ltr) + 20.dp, + end = contentPadding.calculateEndPadding(LayoutDirection.Ltr) + trailingContentWidth, bottom = contentPadding.calculateBottomPadding() ), colors = TextFieldDefaults.colors( @@ -279,22 +291,42 @@ fun DayoPasswordTextField( Box( modifier = Modifier.align(alignment = Alignment.CenterEnd) ) { - if (isError != null && isError == true) { + if (showError) { NoRippleIconButton( onClick = onErrorIconClick, iconContentDescription = "error icon", iconPainter = painterResource(id = errorTrailingIconId), iconButtonModifier = Modifier.size(20.dp) ) - } else { - val trailingIconId = if (passwordHidden) R.drawable.ic_trailing_invisible else R.drawable.ic_trailing_visible - val description = if (passwordHidden) "Show password" else "Hide password" - NoRippleIconButton( - onClick = { passwordHidden = passwordHidden.not() }, - iconContentDescription = description, - iconPainter = painterResource(id = trailingIconId), - iconButtonModifier = Modifier.size(20.dp) - ) + } else if (shouldShowClear || shouldShowVisibility) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (shouldShowClear) { + NoRippleIconButton( + onClick = { onValueChange("") }, + iconContentDescription = "Clear password", + iconPainter = painterResource(id = R.drawable.ic_trailing_delete), + iconButtonModifier = Modifier.size(20.dp) + ) + } + + if (shouldShowVisibility) { + val trailingIconId = if (passwordHidden) { + R.drawable.ic_trailing_invisible + } else { + R.drawable.ic_trailing_visible + } + val description = if (passwordHidden) "Show password" else "Hide password" + NoRippleIconButton( + onClick = { passwordHidden = passwordHidden.not() }, + iconContentDescription = description, + iconPainter = painterResource(id = trailingIconId), + iconButtonModifier = Modifier.size(20.dp) + ) + } + } } } } From a72e58adf77bec4e628731bac7fc0c06d13029e7 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Thu, 12 Mar 2026 01:05:36 +0900 Subject: [PATCH 37/93] [test] Add androidTest AdMob metadata for presentation tests --- presentation/src/androidTest/AndroidManifest.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 presentation/src/androidTest/AndroidManifest.xml diff --git a/presentation/src/androidTest/AndroidManifest.xml b/presentation/src/androidTest/AndroidManifest.xml new file mode 100644 index 000000000..53a7bb10c --- /dev/null +++ b/presentation/src/androidTest/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + + + From ed60689210c3b698dc91320e5b1693ad840279a5 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Thu, 12 Mar 2026 01:05:36 +0900 Subject: [PATCH 38/93] [test] Add auth password field usage coverage --- .../account/AuthPasswordFieldUsageTest.kt | 91 +++++++++++++++++++ .../screen/account/ResetPasswordScreen.kt | 2 +- 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 presentation/src/androidTest/java/daily/dayo/presentation/screen/account/AuthPasswordFieldUsageTest.kt diff --git a/presentation/src/androidTest/java/daily/dayo/presentation/screen/account/AuthPasswordFieldUsageTest.kt b/presentation/src/androidTest/java/daily/dayo/presentation/screen/account/AuthPasswordFieldUsageTest.kt new file mode 100644 index 000000000..1862d3a30 --- /dev/null +++ b/presentation/src/androidTest/java/daily/dayo/presentation/screen/account/AuthPasswordFieldUsageTest.kt @@ -0,0 +1,91 @@ +package daily.dayo.presentation.screen.account + +import androidx.activity.ComponentActivity +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onAllNodesWithContentDescription +import androidx.test.ext.junit.runners.AndroidJUnit4 +import daily.dayo.presentation.theme.DayoTheme +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class AuthPasswordFieldUsageTest { + + @get:Rule + val composeRule = createAndroidComposeRule() + + private fun assertContentDescriptionCount(contentDescription: String, expectedCount: Int) { + composeRule.onAllNodesWithContentDescription(contentDescription) + .assertCountEquals(expectedCount) + } + + @Test + fun signInEmailInputLayout_showsClearAndVisibilityOnEditablePasswordField() { + var email by mutableStateOf("") + var password by mutableStateOf("password") + + composeRule.setContent { + DayoTheme { + SignInEmailInputLayout( + emailValue = email, + onEmailChange = { email = it }, + passwordValue = password, + onPasswordChange = { password = it } + ) + } + } + + assertContentDescriptionCount("Clear password", 1) + assertContentDescriptionCount("Show password", 1) + } + + @Test + fun signUpPasswordConfirmLayout_hidesClearOnDisabledReferenceField() { + var password by mutableStateOf("password") + var passwordConfirmation by mutableStateOf("confirm") + + composeRule.setContent { + DayoTheme { + SetPasswordView( + passwordInputViewCondition = false, + passwordConfirmationViewCondition = true, + password = password, + setPassword = { password = it }, + isPasswordFormatValid = true, + passwordConfirmation = passwordConfirmation, + setPasswordConfirmation = { passwordConfirmation = it } + ) + } + } + + assertContentDescriptionCount("Clear password", 1) + assertContentDescriptionCount("Show password", 2) + } + + @Test + fun resetPasswordConfirmLayout_hidesClearOnDisabledReferenceField() { + var password by mutableStateOf("password") + var passwordConfirmation by mutableStateOf("confirm") + + composeRule.setContent { + DayoTheme { + NewPasswordLayout( + resetPasswordStep = ResetPasswordStep.NEW_PASSWORD_CONFIRM, + password = password, + setPassword = { password = it }, + isPasswordFormatValid = true, + passwordConfirmation = passwordConfirmation, + setPasswordConfirmation = { passwordConfirmation = it } + ) + } + } + + assertContentDescriptionCount("Clear password", 1) + assertContentDescriptionCount("Show password", 2) + } +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt index 8c770ad24..d117a727e 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt @@ -738,7 +738,7 @@ private fun EmailCertificationLayout( @Composable @Preview -private fun NewPasswordLayout( +internal fun NewPasswordLayout( resetPasswordStep: ResetPasswordStep = ResetPasswordStep.NEW_PASSWORD_INPUT, isNextButtonEnabled: Boolean = false, setNextButtonEnabled: (Boolean) -> Unit = {}, From 2b8da498b16575bcb064855fe95bb380d2764731 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Tue, 17 Mar 2026 21:01:31 +0900 Subject: [PATCH 39/93] [chore] Update .coderabbit path filters --- .coderabbit.yaml | 139 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 000000000..a98950a46 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,139 @@ +language: ko-KR +early_access: false +tone_instructions: > + Be concise, constructive, and production-focused. Prioritize architecture + boundaries, crash safety, and maintainability over stylistic trivia. + +reviews: + profile: chill + request_changes_workflow: false + high_level_summary: true + high_level_summary_instructions: | + Summarize the PR by module and risk: + 1) functional impact and user-facing behavior changes, + 2) risk points (network/error handling, threading, state consistency), + 3) required follow-up tests and verification. + review_status: true + review_details: false + commit_status: true + fail_commit_status: false + collapse_walkthrough: false + changed_files_summary: true + sequence_diagrams: false + estimate_code_review_effort: false + suggested_labels: false + auto_apply_labels: false + suggested_reviewers: false + auto_assign_reviewers: false + poem: false + in_progress_fortune: false + path_filters: + - "!**/build/**" + - "!**/.gradle/**" + - "!**/.idea/**" + - "!**/.venv_qa_bot/**" + - "!**/.venv*/**" + - "!**/venv*/**" + - "!**/__pycache__/**" + - "!**/*.pyc" + - "!**/bin/**" + - "!**/gen/**" + - "!**/out/**" + - "!**/obj/**" + - "!**/.externalNativeBuild/**" + - "!**/.signing/**" + - "!**/release/**" + - "!**/*.apk" + - "!**/*.aab" + - "!**/*.ap_" + - "!**/*.dex" + - "!**/*.class" + - "!**/*.iml" + - "!**/*.iws" + - "!**/*.swp" + - "!**/.DS_Store" + - "!**/qa_bot_commit_msg.txt" + - "!**/qa_bot_explanation.txt" + - "!**/pr_body.md" + - "!**/tmp_github_output.txt" + - "!**/.tmp_runner/**" + - "!**/.claude/**" + - "!**/captures/**" + - "!**/.classpath" + - "!**/.project" + - "!**/.cproject" + - "!**/.settings/**" + - "!**/local.properties" + - "!**/sentry.properties" + - "!**/dayo_keystore*" + - "!**/*.jks" + - "!**/*.keystore" + - "!**/*keystore*.properties" + - "!**/crashlytics.properties" + - "!**/crashlytics-build.properties" + - "!**/fabric.properties" + - "!**/com_crashlytics_export_strings.xml" + - "!**/google-services*.json" + - "!**/output-metadata.json" + - "!**/*.log" + path_instructions: + - path: "app/src/main/**/*.kt" + instructions: | + This layer is app-level wiring only. + Review for: + - lifecycle-safe initialization in Application class, + - SDK initialization order and error handling, + - accidental business logic leakage into app module. + - path: "domain/src/main/**/*.kt" + instructions: | + Enforce Domain Layer boundaries. + Ensure: + - no Android imports in domain files, + - repository interfaces stay thin and naming is consistent, + - UseCases are small and usually delegate with `operator fun invoke()`, + - no business logic placed in repository interfaces. + - path: "data/src/main/**/*.kt" + instructions: | + Data layer should be implementation and mapping only. + Check: + - API services are suspend and return `NetworkResponse`, + - `when` handles all 4 `NetworkResponse` cases (no `else`), + - errors are passed through without swallowing, + - mappers convert DTO → domain clearly and handle nullable API fields, + - repository methods remain thin and include DI wiring updates for new APIs. + - path: "presentation/src/main/**/*.kt" + instructions: | + Review for clean Compose/VM architecture. + Verify: + - Route/Screen split is maintained (routing + state in Route, UI-only Screen), + - ViewModel injections stay in Route/RouteOwner only, + - state exposure uses Flow/StateFlow and/or LiveData intentionally, + - one `when` expression handles all `NetworkResponse` branches, + - no hardcoded colors (use theme Color constants), + - no direct navigation from composables using `NavController`. + - path: "**/src/main/res/**/*.xml" + instructions: | + For UI resources and layouts, ensure existing naming conventions and + resource organization are preserved. Avoid introducing unused IDs or + hardcoded values where theme resources already exist. + - path: ".github/**/*" + instructions: | + Check workflow safety and reproducibility: + - no plaintext secrets in files, + - pinned and maintained actions when practical, + - Gradle/JDK matrix changes remain consistent with app requirements. + + auto_review: + enabled: true + auto_incremental_review: true + ignore_title_keywords: + - "WIP" + - "wip" + labels: + - "!wip" + drafts: false + base_branches: + - ".*" + +chat: + auto_reply: true From fe54303e6dae1cd2b186efc2f1cb24f76d32239d Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Tue, 17 Mar 2026 21:18:57 +0900 Subject: [PATCH 40/93] [QA] Fix top navigation bar title text style --- .../main/java/daily/dayo/presentation/view/TopNavigation.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/TopNavigation.kt b/presentation/src/main/java/daily/dayo/presentation/view/TopNavigation.kt index 1e5c56ac9..daa401421 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/TopNavigation.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/TopNavigation.kt @@ -40,7 +40,7 @@ fun TopNavigation( navigationIcon = leftIcon, actions = { rightIcon() }, title = { - Text(text = title, maxLines = 1, style = DayoTheme.typography.h3) + Text(text = title, maxLines = 1, style = DayoTheme.typography.b3) } ) } @@ -55,7 +55,7 @@ fun TopNavigation( navigationIcon = leftIcon, actions = { rightIcon() }, title = { - Text(text = title, maxLines = 1, style = DayoTheme.typography.h3) + Text(text = title, maxLines = 1, style = DayoTheme.typography.b3) } ) } From 39045c431b958426396ee7cd8ae8d195d52aa417 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Tue, 17 Mar 2026 21:19:37 +0900 Subject: [PATCH 41/93] [QA] Add Missing Top Navigation bar title alignment parameter --- .../dayo/presentation/screen/settings/BlockedUsersScreen.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/settings/BlockedUsersScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/settings/BlockedUsersScreen.kt index 3869a2055..cf79adc62 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/settings/BlockedUsersScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/settings/BlockedUsersScreen.kt @@ -49,6 +49,7 @@ import daily.dayo.presentation.view.FilledRoundedCornerButton import daily.dayo.presentation.view.NoRippleIconButton import daily.dayo.presentation.view.RoundImageView import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.TopNavigationAlign import daily.dayo.presentation.viewmodel.ProfileSettingViewModel import daily.dayo.presentation.viewmodel.ProfileViewModel import kotlinx.coroutines.launch @@ -261,5 +262,6 @@ fun BlockedUsersActionbarLayout( ) }, title = stringResource(R.string.blocked_users_title), + titleAlignment = TopNavigationAlign.CENTER, ) } \ No newline at end of file From 172ddbdb09dbbd5f071f6a36ef5d0c9b2f36b601 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Tue, 17 Mar 2026 21:33:55 +0900 Subject: [PATCH 42/93] [QA] Fix Retry Layout, modify retry text string/error image --- .../screen/settings/BlockedUsersScreen.kt | 104 ++++++++++-------- .../res/drawable/ic_blocked_users_empty.xml | 49 ++------- presentation/src/main/res/values/strings.xml | 2 +- 3 files changed, 69 insertions(+), 86 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/settings/BlockedUsersScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/settings/BlockedUsersScreen.kt index cf79adc62..465f48788 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/settings/BlockedUsersScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/settings/BlockedUsersScreen.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -97,19 +98,25 @@ fun BlockedUsersScreen( topBar = { BlockedUsersActionbarLayout(onBackClick = onBackClick) }, snackbarHost = { SnackbarHost(snackBarHostState) }, content = { innerPadding -> - Column( + Box( modifier = Modifier .background(DayoTheme.colorScheme.background) .padding(innerPadding) .fillMaxSize() - .padding(top = 12.dp, start = 20.dp, end = 20.dp) + .padding(top = 12.dp) ) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues(vertical = 16.dp) - ) { - if (blockedUsers.status != Status.ERROR) { + if (blockedUsers.status == Status.ERROR) { + BlockedUsersErrorLayout( + onRetry = { profileSettingViewModel.requestBlockList() }, + ) + } else { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(start = 20.dp, end = 20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(vertical = 16.dp) + ) { blockedUsers.data.orEmpty().let { blockedUsers -> if (blockedUsers.isEmpty()) { item { @@ -160,41 +167,6 @@ fun BlockedUsersScreen( } } } - } else { - item { - Column( - modifier = Modifier - .fillMaxSize() - .padding(top = 164.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Image( - painter = painterResource(id = R.drawable.ic_blocked_users_empty), - contentDescription = null, - modifier = Modifier - .width(136.dp) - .wrapContentHeight() - .padding(6.5.dp) - ) - Spacer(modifier = Modifier.height(20.dp)) - Text( - text = stringResource(R.string.blocked_users_error_description), - color = Gray3_9FA5AE, - style = DayoTheme.typography.b3, - modifier = Modifier - .wrapContentSize() - ) - Spacer(modifier = Modifier.height(20.dp)) - FilledRoundedCornerButton( - modifier = Modifier - .padding(horizontal = 20.dp) - .wrapContentSize(), - onClick = { profileSettingViewModel.requestBlockList() }, - label = stringResource(R.string.re_try) - ) - } - } } } } @@ -202,6 +174,50 @@ fun BlockedUsersScreen( ) } +@Composable +private fun BlockedUsersErrorLayout( + onRetry: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.weight(176f)) + + Column( + modifier = Modifier.wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(id = R.drawable.ic_blocked_users_empty), + contentDescription = null, + modifier = Modifier + .width(136.dp) + .height(100.dp) + ) + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = stringResource(R.string.blocked_users_error_description), + style = DayoTheme.typography.h3.copy(color = Gray3_9FA5AE), + modifier = Modifier.wrapContentSize() + ) + } + + Spacer(modifier = Modifier.weight(336f)) + + FilledRoundedCornerButton( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .padding(bottom = 20.dp), + onClick = onRetry, + label = stringResource(R.string.re_try), + ) + } +} + @Preview @Composable fun BlockedUser( @@ -264,4 +280,4 @@ fun BlockedUsersActionbarLayout( title = stringResource(R.string.blocked_users_title), titleAlignment = TopNavigationAlign.CENTER, ) -} \ No newline at end of file +} diff --git a/presentation/src/main/res/drawable/ic_blocked_users_empty.xml b/presentation/src/main/res/drawable/ic_blocked_users_empty.xml index 648d468e6..144752c33 100644 --- a/presentation/src/main/res/drawable/ic_blocked_users_empty.xml +++ b/presentation/src/main/res/drawable/ic_blocked_users_empty.xml @@ -1,42 +1,9 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 236dd6e15..61e0f4cb2 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -28,7 +28,7 @@ 잠시만 기다려 주세요 인터넷 연결 상태를 확인해주세요 닫기 - 재시도 + 다시 시도 방금 전 From 14590b69d3c41f81a684ceed5637f5010629fdeb Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Wed, 18 Mar 2026 20:40:00 +0900 Subject: [PATCH 43/93] [QA] Fix SearchResultCount text style, padding --- .../screen/search/SearchResultScreen.kt | 24 +++++++++++-------- presentation/src/main/res/values/strings.xml | 1 + 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt index 0f73c6055..c1499b41e 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt @@ -351,33 +351,37 @@ fun SearchResultEmpty() { @Preview fun SearchResultsCount(resultCount: Int = 0) { Surface( - color = White_FFFFFF, modifier = Modifier .fillMaxWidth() - .height(44.dp), + .height(44.dp) + .background(White_FFFFFF) + .padding(horizontal = 18.dp, vertical = 12.dp) ) { Row( - horizontalArrangement = Arrangement.spacedBy(0.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(horizontal = 18.dp, vertical = 12.dp) + modifier = Modifier.height(20.dp) ) { Text( + text = "$resultCount", + modifier = Modifier.padding(end = 2.dp), style = TextStyle( fontSize = 13.sp, + lineHeight = 19.5.sp, fontFamily = FontFamily(Font(R.font.pretendard_medium)), fontWeight = FontWeight(500), color = Primary_23C882 - ), - text = "$resultCount", - modifier = Modifier.padding(end = 2.dp) + ) ) Text( + text = stringResource(R.string.search_result_count_description), style = TextStyle( fontSize = 13.sp, + lineHeight = 19.5.sp, fontFamily = FontFamily(Font(R.font.pretendard_medium)), - fontWeight = FontWeight(500) - ), - text = "개의 검색결과" + fontWeight = FontWeight(500), + color = Gray2_767B83, + ) ) } } diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 236dd6e15..b27906220 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -259,6 +259,7 @@ 추천 검색어 앗! 찾으시는 검색 결과가 없어요. 다른 검색어를 입력해보세요 + 개의 검색 결과 검색 기록 지우기 최근 검색 결과가 없어요. 관심있는 키워드 또는 사용자를 찾아보세요 From 09827e12b34bca437f6e90dfb72c39002d1e2ed4 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Wed, 18 Mar 2026 20:47:35 +0900 Subject: [PATCH 44/93] [QA] Add background color on SearchResultsCount --- .../dayo/presentation/screen/search/SearchResultScreen.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt index c1499b41e..bf8d77450 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt @@ -360,7 +360,9 @@ fun SearchResultsCount(resultCount: Int = 0) { Row( horizontalArrangement = Arrangement.spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.height(20.dp) + modifier = Modifier + .height(20.dp) + .background(White_FFFFFF) ) { Text( text = "$resultCount", From ca55d9b8ec91d8de072d5721c25667920c935cd0 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Wed, 18 Mar 2026 20:55:19 +0900 Subject: [PATCH 45/93] #1024 [QA] Fix Divider color, thickness on SearchResult tab --- .../dayo/presentation/screen/search/SearchResultScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt index bf8d77450..dfae772b9 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt @@ -50,7 +50,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color.Companion.White import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale @@ -87,6 +86,7 @@ import daily.dayo.presentation.theme.Gray1_50545B import daily.dayo.presentation.theme.Gray2_767B83 import daily.dayo.presentation.theme.Gray3_9FA5AE import daily.dayo.presentation.theme.Gray5_E8EAEE +import daily.dayo.presentation.theme.Gray6_F0F1F3 import daily.dayo.presentation.theme.PrimaryL3_F2FBF7 import daily.dayo.presentation.theme.Primary_23C882 import daily.dayo.presentation.theme.White_FFFFFF @@ -231,7 +231,7 @@ fun SearchResultScreen( color = Primary_23C882, ) }, - divider = { Divider(color = Color.Transparent, thickness = 0.dp) }, + divider = { Divider(color = Gray6_F0F1F3, thickness = 1.dp) }, modifier = Modifier .fillMaxWidth() .padding(18.dp, 0.dp, 18.dp, 0.dp), From 75d3464cbf4e0947405a8c3a4b261d5f86a46857 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Wed, 18 Mar 2026 21:15:45 +0900 Subject: [PATCH 46/93] #1026 [QA] Fix spacing in SearchResultEmpty --- .../daily/dayo/presentation/screen/search/SearchResultScreen.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt index dfae772b9..28074dfab 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt @@ -335,6 +335,7 @@ fun SearchResultEmpty() { textAlign = TextAlign.Center ), ) + Spacer(modifier = Modifier.height(2.dp)) Text( modifier = Modifier.padding(vertical = 2.dp), text = stringResource(id = R.string.search_result_empty_description), From cff4b314aa6fa41e9682ffa3bdbaceaecf76ac30 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Wed, 18 Mar 2026 21:30:57 +0900 Subject: [PATCH 47/93] #1027 [QA] Fix font weight and color for search result description --- .../presentation/screen/search/SearchResultScreen.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt index 28074dfab..7cbe25144 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt @@ -85,6 +85,7 @@ import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray1_50545B import daily.dayo.presentation.theme.Gray2_767B83 import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Gray4_C5CAD2 import daily.dayo.presentation.theme.Gray5_E8EAEE import daily.dayo.presentation.theme.Gray6_F0F1F3 import daily.dayo.presentation.theme.PrimaryL3_F2FBF7 @@ -337,13 +338,11 @@ fun SearchResultEmpty() { ) Spacer(modifier = Modifier.height(2.dp)) Text( - modifier = Modifier.padding(vertical = 2.dp), text = stringResource(id = R.string.search_result_empty_description), - style = DayoTheme.typography.caption1 - .copy( - color = Gray3_9FA5AE, - textAlign = TextAlign.Center - ), + modifier = Modifier.padding(vertical = 2.dp), + color = Gray4_C5CAD2, + fontWeight = FontWeight(500), + style = DayoTheme.typography.caption2, ) } } From 5584e11ebab49336912c6bc185626a3a822e81e3 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Wed, 18 Mar 2026 21:51:14 +0900 Subject: [PATCH 48/93] [QA] Remove duplicate padding --- .../daily/dayo/presentation/screen/search/SearchResultScreen.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt index 7cbe25144..24940a43f 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt @@ -339,7 +339,6 @@ fun SearchResultEmpty() { Spacer(modifier = Modifier.height(2.dp)) Text( text = stringResource(id = R.string.search_result_empty_description), - modifier = Modifier.padding(vertical = 2.dp), color = Gray4_C5CAD2, fontWeight = FontWeight(500), style = DayoTheme.typography.caption2, @@ -366,7 +365,6 @@ fun SearchResultsCount(resultCount: Int = 0) { ) { Text( text = "$resultCount", - modifier = Modifier.padding(end = 2.dp), style = TextStyle( fontSize = 13.sp, lineHeight = 19.5.sp, From 2298e029e667714b676cc1b0f262acc72665a812 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Wed, 25 Mar 2026 21:06:14 +0900 Subject: [PATCH 49/93] #1043 [QA] Change title text color --- .../java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt index d11514c0c..ce8ce02dd 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt @@ -372,7 +372,7 @@ private fun MyPageTopNavigation(onSettingsClick: () -> Unit) { Text( text = stringResource(id = R.string.my_page), style = DayoTheme.typography.h1.copy( - color = Gray1_50545B, + color = Dark, fontWeight = FontWeight.SemiBold ) ) From c388be394ccae40effdf117830cfebd7f6859e9c Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Wed, 25 Mar 2026 21:26:17 +0900 Subject: [PATCH 50/93] #1044 [QA] Fix setting icon --- .../presentation/screen/mypage/MyPageScreen.kt | 5 +++-- .../src/main/res/drawable/ic_setting.xml | 17 +++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt index ce8ce02dd..757908a99 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt @@ -383,9 +383,10 @@ private fun MyPageTopNavigation(onSettingsClick: () -> Unit) { onClick = onSettingsClick, iconContentDescription = "setting button", iconPainter = painterResource(id = R.drawable.ic_setting), + iconButtonModifier = Modifier.padding(end = 8.dp), iconModifier = Modifier - .padding(end = 18.dp) - .size(24.dp), + .size(44.dp) + .padding(10.dp), iconTintColor = Dark ) } diff --git a/presentation/src/main/res/drawable/ic_setting.xml b/presentation/src/main/res/drawable/ic_setting.xml index ac866b614..9ef66caf1 100644 --- a/presentation/src/main/res/drawable/ic_setting.xml +++ b/presentation/src/main/res/drawable/ic_setting.xml @@ -1,9 +1,14 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + android:pathData="M5.42 3.1C4.46 3.65 4.13 4.87 4.69 5.83l0.23 0.41C5.12 6.6 5.1 7.03 4.87 7.37c-0.16 0.25-0.31 0.5-0.45 0.77C4.24 8.5 3.88 8.75 3.48 8.75H3c-1.1 0-2 0.9-2 2v2.5c0 1.1 0.9 2 2 2h0.48c0.4 0 0.76 0.25 0.94 0.6 0.14 0.27 0.29 0.53 0.45 0.78 0.22 0.34 0.26 0.78 0.05 1.13l-0.23 0.4c-0.56 0.97-0.23 2.19 0.73 2.74l2.16 1.25c0.96 0.55 2.18 0.23 2.73-0.73L10.55 21c0.2-0.35 0.6-0.54 1-0.52L12 20.5l0.45-0.01c0.4-0.02 0.8 0.17 1 0.52l0.24 0.4c0.55 0.97 1.77 1.3 2.73 0.74l2.16-1.25c0.96-0.55 1.29-1.77 0.73-2.73l-0.23-0.41c-0.2-0.35-0.17-0.79 0.05-1.13 0.16-0.25 0.31-0.5 0.45-0.77 0.18-0.36 0.54-0.61 0.94-0.61H21c1.1 0 2-0.9 2-2v-2.5c0-1.1-0.9-2-2-2h-0.48c-0.4 0-0.76-0.25-0.94-0.6-0.14-0.27-0.29-0.53-0.45-0.78-0.22-0.34-0.26-0.78-0.05-1.13l0.23-0.4c0.56-0.97 0.23-2.19-0.73-2.74l-2.16-1.25c-0.96-0.55-2.18-0.23-2.73 0.73L13.45 3c-0.2 0.35-0.6 0.54-1 0.52L12 3.5l-0.45 0.01c-0.4 0.02-0.8-0.17-1-0.52l-0.24-0.41C9.76 1.62 8.54 1.3 7.58 1.85L5.42 3.1Z" + android:strokeWidth="1.4" + android:strokeColor="#FF313131" /> + \ No newline at end of file From b39d0de19d92ec3b731ac68d4c3085c025a0eebd Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Wed, 1 Apr 2026 20:10:07 +0900 Subject: [PATCH 51/93] #1048 [QA] Fix line height for folder title --- .../java/daily/dayo/presentation/view/FolderView.kt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/FolderView.kt b/presentation/src/main/java/daily/dayo/presentation/view/FolderView.kt index d49cce7e6..6727d234a 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/FolderView.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/FolderView.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import daily.dayo.domain.model.Folder import daily.dayo.domain.model.Privacy import daily.dayo.presentation.BuildConfig @@ -77,10 +78,18 @@ fun FolderView( // folder info Column { - Text(text = folder.title, style = DayoTheme.typography.b6.copy(Dark)) + Text( + text = folder.title, + lineHeight = 21.sp, + style = DayoTheme.typography.b6.copy(Dark) + ) val dec = DecimalFormat("#,###") - Text(text = "${dec.format(folder.postCount)}개", style = DayoTheme.typography.b6.copy(Gray3_9FA5AE)) + Text( + text = "${dec.format(folder.postCount)}개", + lineHeight = 21.sp, + style = DayoTheme.typography.b6.copy(Gray3_9FA5AE) + ) } } } \ No newline at end of file From 4e70dfccdda307ecb197775c007c945b0f956580 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Wed, 1 Apr 2026 20:24:33 +0900 Subject: [PATCH 52/93] #1047 [QA] Fix bookmark icon size on My Page --- .../java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt index 757908a99..85c870117 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt @@ -299,6 +299,7 @@ private fun MyPageMenu( Icon( painter = painterResource(R.drawable.ic_bookmark_default), contentDescription = stringResource(id = R.string.bookmark), + modifier = Modifier.size(20.dp), tint = Gray1_50545B ) } From 34b005e039d3b2da7a5c566dc7bffff4b45ac829 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Wed, 1 Apr 2026 20:35:43 +0900 Subject: [PATCH 53/93] #1047 [QA] Fix button icon and text style on MyPage --- .../daily/dayo/presentation/screen/mypage/MyPageScreen.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt index 85c870117..3716b7f37 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt @@ -340,12 +340,13 @@ private fun MyPageDiaryHeader( ) { Icon( imageVector = Icons.Filled.Add, - contentDescription = stringResource(id = R.string.my_profile_new_folder) + contentDescription = stringResource(id = R.string.my_profile_new_folder), + modifier = Modifier.size(12.dp) ) Spacer(modifier = Modifier.width(4.dp)) Text( text = stringResource(id = R.string.my_profile_new_folder), - style = DayoTheme.typography.b6.copy( + style = DayoTheme.typography.caption4.copy( if (isCreateFolderEnabled) Primary_23C882 else Gray4_C5CAD2 ) ) From e1a318b7307878e89df4f1517815282c6e3dec81 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Tue, 7 Apr 2026 22:02:39 +0900 Subject: [PATCH 54/93] #1049 [QA] Fix entire thumbnail card clickable --- .../daily/dayo/presentation/screen/bookmark/BookmarkScreen.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/bookmark/BookmarkScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/bookmark/BookmarkScreen.kt index 57079a5f4..3560d5dfd 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/bookmark/BookmarkScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/bookmark/BookmarkScreen.kt @@ -224,7 +224,9 @@ private fun BookmarkPostItem( isSelected: Boolean, onBookmarkClick: () -> Unit ) { - Box { + Box( + modifier = Modifier.clickableSingle(onClick = { onBookmarkClick() }) + ) { RoundImageView( context = LocalContext.current, imageUrl = "${BuildConfig.BASE_URL}/images/${post.thumbnailImage}", From c659d30bcfe687f27e53751390e013e87cf8dc2e Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Tue, 7 Apr 2026 22:22:14 +0900 Subject: [PATCH 55/93] #1050 [QA] Add modifier in ToggleButtonWithLabel --- .../main/java/daily/dayo/presentation/view/Switch.kt | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/Switch.kt b/presentation/src/main/java/daily/dayo/presentation/view/Switch.kt index 9578b159d..e8beb9cee 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/Switch.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/Switch.kt @@ -3,10 +3,7 @@ package daily.dayo.presentation.view import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material.Text import androidx.compose.material3.Switch import androidx.compose.material3.SwitchDefaults @@ -24,15 +21,13 @@ import daily.dayo.presentation.theme.White_FFFFFF fun ToggleButtonWithLabel( label: String, isToggled: Boolean, - onToggleChanged: (Boolean) -> Unit + onToggleChanged: (Boolean) -> Unit, + modifier: Modifier = Modifier ) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End, - modifier = Modifier - .padding(18.dp) - .fillMaxWidth() - .wrapContentHeight() + modifier = modifier ) { Text( text = label, From 024f9a2724fbf35925c41e26dd5e0274ac3aee79 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Tue, 7 Apr 2026 22:23:36 +0900 Subject: [PATCH 56/93] #1050 [QA] Fix horizontal padding in FolderCreateScreen --- .../dayo/presentation/screen/folder/FolderCreateScreen.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderCreateScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderCreateScreen.kt index 2a407133d..335ed7f70 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderCreateScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderCreateScreen.kt @@ -107,7 +107,7 @@ private fun FolderCreateScreen( .background(DayoTheme.colorScheme.background) .fillMaxSize() .padding(contentPadding) - .padding(horizontal = 18.dp, vertical = 16.dp), + .padding(horizontal = 20.dp, vertical = 16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { DayoTextField( @@ -143,7 +143,10 @@ private fun FolderCreateScreen( ToggleButtonWithLabel( label = stringResource(R.string.write_post_folder_new_folder_privacy_title), isToggled = privacy.value == Privacy.ONLY_ME, - onToggleChanged = { privacy.value = if (it) Privacy.ONLY_ME else Privacy.ALL } + onToggleChanged = { privacy.value = if (it) Privacy.ONLY_ME else Privacy.ALL }, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.End), ) } } From 63089575815bb998a1f63d6e4d4415ff20f744e6 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Wed, 8 Apr 2026 21:35:46 +0900 Subject: [PATCH 57/93] [feature] Add navigate to bookmark post --- .../screen/bookmark/BookmarkScreen.kt | 20 +++++++++++++------ .../screen/mypage/MypageNavigation.kt | 1 + 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/bookmark/BookmarkScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/bookmark/BookmarkScreen.kt index 3560d5dfd..bfbcdddb7 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/bookmark/BookmarkScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/bookmark/BookmarkScreen.kt @@ -50,6 +50,7 @@ import java.text.DecimalFormat @Composable fun BookmarkScreen( + onPostClick: (Long) -> Unit, onBackClick: () -> Unit, bookmarkViewModel: BookmarkViewModel = hiltViewModel() ) { @@ -105,7 +106,8 @@ fun BookmarkScreen( post = post, isEditMode = bookmarkUiState.isEditMode, isSelected = bookmarkUiState.selectedBookmarks.contains(post.postId), - onBookmarkClick = { bookmarkViewModel.toggleSelection(post.postId) } + onBookmarkPostClick = { onPostClick(post.postId) }, + onBookmarkEditClick = { bookmarkViewModel.toggleSelection(post.postId) } ) } } @@ -222,11 +224,10 @@ private fun BookmarkPostItem( post: BookmarkPost, isEditMode: Boolean, isSelected: Boolean, - onBookmarkClick: () -> Unit + onBookmarkPostClick: (BookmarkPost) -> Unit, + onBookmarkEditClick: () -> Unit ) { - Box( - modifier = Modifier.clickableSingle(onClick = { onBookmarkClick() }) - ) { + Box { RoundImageView( context = LocalContext.current, imageUrl = "${BuildConfig.BASE_URL}/images/${post.thumbnailImage}", @@ -234,12 +235,19 @@ private fun BookmarkPostItem( modifier = Modifier .fillMaxWidth() .aspectRatio(1f) + .clickableSingle(onClick = { + if (isEditMode) { + onBookmarkEditClick() + } else { + onBookmarkPostClick(post) + } + }) ) if (isEditMode) { DayoCheckbox( checked = isSelected, - onCheckedChange = { onBookmarkClick() }, + onCheckedChange = { onBookmarkEditClick() }, modifier = Modifier.align(Alignment.TopEnd) ) } diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MypageNavigation.kt b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MypageNavigation.kt index 130a9208b..04d9134e4 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MypageNavigation.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MypageNavigation.kt @@ -125,6 +125,7 @@ fun NavGraphBuilder.myPageNavGraph( composable(MyPageRoute.bookmark()) { BookmarkScreen( + onPostClick = onPostClick, onBackClick = onBackClick ) } From 505b7335f2ab89333a1c93ef1be3a22c22df0b7a Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Sun, 19 Apr 2026 17:30:52 +0900 Subject: [PATCH 58/93] #1036 [layout] Fix image edit button text wrapping --- .../presentation/screen/write/WriteScreen.kt | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteScreen.kt index 693aa1fe2..8571d126c 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteScreen.kt @@ -31,7 +31,9 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.TextAutoSize import androidx.compose.material.Surface +import androidx.compose.material3.Icon import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SnackbarHostState @@ -64,6 +66,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import androidx.core.content.ContextCompat import androidx.hilt.navigation.compose.hiltViewModel @@ -445,26 +448,33 @@ fun WriteUploadImages( ) { Row( modifier = Modifier - .width(112.dp) + .width(116.dp) .height(36.dp) .clickable { onEditImage(index) } - .padding(horizontal = 12.dp) + .padding(horizontal = 12.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically ) { - Image( - painter = painterResource(id = R.drawable.ic_crop), - contentDescription = "edit image", - modifier = Modifier - .align(Alignment.CenterVertically) - .size(20.dp) + Icon( + painter = painterResource(id = R.drawable.ic_plus_green), + contentDescription = stringResource(R.string.write_post_image_edit), + tint = White_FFFFFF, + modifier = Modifier.size(20.dp) ) Spacer(modifier = Modifier.width(4.dp)) Text( text = stringResource(R.string.write_post_image_edit), style = DayoTheme.typography.b5, color = White_FFFFFF, - modifier = Modifier.align(Alignment.CenterVertically) + autoSize = TextAutoSize.StepBased( + minFontSize = 12.sp, + maxFontSize = 14.sp, + stepSize = 0.25.sp, + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis ) } } @@ -787,4 +797,4 @@ fun WriteFolderLayout( contentDescription = stringResource(R.string.write_post_select_folder_title) ) } -} \ No newline at end of file +} From b763963e2e218e0b65ba80121c5bdf7d7eeb232d Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Sun, 19 Apr 2026 17:58:12 +0900 Subject: [PATCH 59/93] #1037 [QA] Fix tag summary truncation in Write_tag_complete --- .../daily/dayo/presentation/screen/write/WriteScreen.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteScreen.kt index 8571d126c..682e74e06 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteScreen.kt @@ -688,13 +688,9 @@ fun WriteTagLayout( style = DayoTheme.typography.b3, color = Dark ) - Spacer( - modifier = Modifier - .weight(1f) - .widthIn(min = 54.dp) - ) + Spacer(modifier = Modifier.width(12.dp)) if (tags.isNotEmpty()) { - val tag = tags.joinToString(separator = ", ", postfix = " ") { + val tag = tags.joinToString(separator = ", ") { ContextCompat.getString(context, R.string.write_post_select_tag_contents).format(it) } Text( From d149375c0bc5a4eae0aac499450a1f806a594a99 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Sun, 19 Apr 2026 18:48:10 +0900 Subject: [PATCH 60/93] #1038 [QA] Fix selected check icon in Write_folder_selected --- .../screen/write/WriteFolderScreen.kt | 22 +++++++++---------- .../res/drawable/ic_check_corner_round.xml | 15 +++++++++++++ 2 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 presentation/src/main/res/drawable/ic_check_corner_round.xml diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderScreen.kt index ab809b720..0d00a9714 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderScreen.kt @@ -290,17 +290,17 @@ fun WriteFolderItemLayout( .clip(RoundedCornerShape(size = FOLDER_THUMBNAIL_RADIUS_SIZE.dp)) .background(Primary_23C882.copy(alpha = 0.6f)) ) { - Image( - painter = painterResource(id = R.drawable.ic_check), - contentDescription = "Selected", - contentScale = ContentScale.Crop, - colorFilter = ColorFilter.tint(White_FFFFFF), - modifier = Modifier - .size(18.dp) - .align(Alignment.Center) - ) - } - } + Image( + painter = painterResource(id = R.drawable.ic_check_corner_round), + contentDescription = "Selected", + contentScale = ContentScale.Fit, + colorFilter = ColorFilter.tint(White_FFFFFF), + modifier = Modifier + .align(Alignment.Center) + .size(18.dp) + ) + } + } } Spacer(modifier = Modifier.width(12.dp)) Column( diff --git a/presentation/src/main/res/drawable/ic_check_corner_round.xml b/presentation/src/main/res/drawable/ic_check_corner_round.xml new file mode 100644 index 000000000..4b9572185 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_check_corner_round.xml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file From 2f0a8d39ff6962b45482e1e0a778739fb784a0a1 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Sun, 19 Apr 2026 18:56:32 +0900 Subject: [PATCH 61/93] #1039 [QA] Fix private folder lock icon in Write_folder_selected --- .../screen/write/WriteFolderScreen.kt | 13 +++++-------- presentation/src/main/res/drawable/ic_lock.xml | 18 ++++-------------- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderScreen.kt index 0d00a9714..93fa5b57b 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderScreen.kt @@ -51,6 +51,7 @@ import daily.dayo.presentation.common.extension.clickableSingle import daily.dayo.presentation.common.extension.limitTo import daily.dayo.presentation.theme.Dark import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray1_50545B import daily.dayo.presentation.theme.Gray2_767B83 import daily.dayo.presentation.theme.Gray3_9FA5AE import daily.dayo.presentation.theme.Primary_23C882 @@ -308,24 +309,20 @@ fun WriteFolderItemLayout( .fillMaxHeight() .wrapContentHeight(Alignment.CenterVertically) ) { - Row { + Row(verticalAlignment = Alignment.CenterVertically) { if (folder.privacy == Privacy.ONLY_ME) { Image( modifier = Modifier - .height(24.dp) - .wrapContentHeight(Alignment.CenterVertically), + .size(16.dp), painter = painterResource(id = R.drawable.ic_lock), contentDescription = "Only Me", - colorFilter = ColorFilter.tint(Dark) + colorFilter = ColorFilter.tint(Gray1_50545B) ) Spacer(modifier = Modifier.width(4.dp)) } Text( modifier = Modifier - .fillMaxWidth() - .height(24.dp) - .wrapContentWidth(Alignment.Start) - .wrapContentHeight(Alignment.CenterVertically), + .fillMaxWidth(), text = folder.title, style = DayoTheme.typography.b4.copy( color = Dark, diff --git a/presentation/src/main/res/drawable/ic_lock.xml b/presentation/src/main/res/drawable/ic_lock.xml index f760683fd..455f0a607 100644 --- a/presentation/src/main/res/drawable/ic_lock.xml +++ b/presentation/src/main/res/drawable/ic_lock.xml @@ -1,15 +1,5 @@ - - - + + + + From f52197bf3f84dbb077ad6665b1ba6cbdfe68304b Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Sun, 19 Apr 2026 20:38:37 +0900 Subject: [PATCH 62/93] #1040 [QA] Fix default folder thumbnail in Write_folder_selected --- .../screen/write/WriteFolderScreen.kt | 8 ++++- .../drawable/img_default_folder_dayo_logo.xml | 35 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 presentation/src/main/res/drawable/img_default_folder_dayo_logo.xml diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderScreen.kt index 93fa5b57b..f72d9b22b 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderScreen.kt @@ -265,6 +265,11 @@ fun WriteFolderItemLayout( isSelected: Boolean = true, onFolderClick: (Long, String) -> Unit = { _, _ -> }, ) { + val thumbnailModel: Any = folder.thumbnailImage + .takeIf { it.isNotBlank() } + ?.let { "${BuildConfig.BASE_URL}/images/$it" } + ?: R.drawable.img_default_folder_dayo_logo + Row( modifier = Modifier .fillMaxWidth() @@ -279,10 +284,11 @@ fun WriteFolderItemLayout( ) { RoundImageView( context = LocalContext.current, - imageUrl = "${BuildConfig.BASE_URL}/images/${folder.thumbnailImage}", + imageUrl = thumbnailModel, modifier = Modifier.size(FOLDER_THUMBNAIL_SIZE.dp), imageSize = Size(FOLDER_THUMBNAIL_SIZE, FOLDER_THUMBNAIL_SIZE), roundSize = FOLDER_THUMBNAIL_RADIUS_SIZE.dp, + placeholderResId = R.drawable.img_default_folder_dayo_logo, ) if (isSelected) { Box( diff --git a/presentation/src/main/res/drawable/img_default_folder_dayo_logo.xml b/presentation/src/main/res/drawable/img_default_folder_dayo_logo.xml new file mode 100644 index 000000000..8addc5687 --- /dev/null +++ b/presentation/src/main/res/drawable/img_default_folder_dayo_logo.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + From b9943e869d5d84799a29cb813896b634244103ca Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Sun, 19 Apr 2026 22:07:51 +0900 Subject: [PATCH 63/93] #1041 [QA] Fix category bottom sheet spacing in Write_category --- .../dayo/presentation/view/dialog/BottomSheetDialog.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt index 156533d17..c20c3a78e 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt @@ -12,8 +12,8 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape @@ -127,7 +127,7 @@ fun BottomSheetDialog( interactionSource = interactionSource ) .background(White_FFFFFF) - .padding(12.dp), + .padding(vertical = 8.dp, horizontal = 12.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { @@ -139,14 +139,14 @@ fun BottomSheetDialog( tint = Color.Unspecified ) } + Spacer(modifier = Modifier.width(12.dp)) Text( text = button.first, - modifier = Modifier.offset(8.dp, 0.dp), + modifier = Modifier.weight(1f), color = if ((isFirstButtonColored && index == 0) || (checkedButtonIndex == index)) checkedColor else normalColor, fontSize = 16.sp, style = DayoTheme.typography.b4 ) - Spacer(modifier = Modifier.weight(1f)) if (checkedButtonIndex == index) { Icon( imageVector = rightIcon, @@ -241,4 +241,4 @@ fun PreviewMyBottomSheetDialog() { checkedButtonIndex = 0, ) } -} \ No newline at end of file +} From 4dcfc9e8159a80c324ddfa5e2267b30a7f558ef3 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Sun, 19 Apr 2026 22:21:38 +0900 Subject: [PATCH 64/93] [QA] Unify write summary spacing --- .../dayo/presentation/screen/write/WriteScreen.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteScreen.kt index 682e74e06..ce30ca565 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteScreen.kt @@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed @@ -102,6 +101,8 @@ const val WRITE_POST_DETAIL_MAX_LENGTH = 200 const val WRITE_POST_IMAGE_SIZE = 220 const val WRITE_POST_TOP_Z_INDEX = 1f +private val WriteSummaryLabelSpacing = 12.dp + @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun WriteRoute( @@ -688,7 +689,7 @@ fun WriteTagLayout( style = DayoTheme.typography.b3, color = Dark ) - Spacer(modifier = Modifier.width(12.dp)) + Spacer(modifier = Modifier.width(WriteSummaryLabelSpacing)) if (tags.isNotEmpty()) { val tag = tags.joinToString(separator = ", ") { ContextCompat.getString(context, R.string.write_post_select_tag_contents).format(it) @@ -753,11 +754,7 @@ fun WriteFolderLayout( style = DayoTheme.typography.b3, color = Dark ) - Spacer( - modifier = Modifier - .weight(1f) - .widthIn(min = 54.dp) - ) + Spacer(modifier = Modifier.width(WriteSummaryLabelSpacing)) if (!folderName.isNullOrEmpty()) { Text( modifier = Modifier From 9081bd0fa4717e74194d7269726b473870e0f605 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Mon, 20 Apr 2026 21:49:46 +0900 Subject: [PATCH 65/93] [QA] Fix label color in TextField --- .../java/daily/dayo/presentation/view/TextField.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/TextField.kt b/presentation/src/main/java/daily/dayo/presentation/view/TextField.kt index 3356c721e..be35c50c8 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/TextField.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/TextField.kt @@ -53,6 +53,7 @@ import daily.dayo.presentation.common.TextLimitUtil import daily.dayo.presentation.theme.Dark import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray3_9FA5AE import daily.dayo.presentation.theme.Gray4_C5CAD2 import daily.dayo.presentation.theme.Gray5_E8EAEE import daily.dayo.presentation.theme.Gray6_F0F1F3 @@ -90,7 +91,7 @@ fun DayoTextField( Text( text = label, style = DayoTheme.typography.caption3.copy( - color = Gray4_C5CAD2, + color = Gray3_9FA5AE, fontWeight = FontWeight.SemiBold ) ) @@ -143,7 +144,7 @@ fun DayoTextField( errorContainerColor = Color.Transparent, focusedContainerColor = Color.Transparent, unfocusedLabelColor = Color.Transparent, // 라벨 - focusedLabelColor = Gray4_C5CAD2, + focusedLabelColor = Gray3_9FA5AE, errorLabelColor = Red_FF4545, focusedPlaceholderColor = Gray5_E8EAEE, // 힌트 unfocusedPlaceholderColor = Gray5_E8EAEE, @@ -224,7 +225,7 @@ fun DayoPasswordTextField( Text( text = label, style = DayoTheme.typography.caption3.copy( - color = Gray4_C5CAD2, + color = Gray3_9FA5AE, fontWeight = FontWeight.SemiBold ) ) @@ -278,7 +279,7 @@ fun DayoPasswordTextField( errorContainerColor = Color.Transparent, focusedContainerColor = Color.Transparent, unfocusedLabelColor = Color.Transparent, // 라벨 - focusedLabelColor = Gray4_C5CAD2, + focusedLabelColor = Gray3_9FA5AE, errorLabelColor = Red_FF4545, focusedPlaceholderColor = Gray5_E8EAEE, // 힌트 unfocusedPlaceholderColor = Gray5_E8EAEE, @@ -355,7 +356,7 @@ fun DayoTimerTextField( isError: Boolean = false, errorMessage: String = "", timeOutErrorMessage: String = stringResource(id = R.string.email_address_certificate_alert_message_time_fail), - labelColor: Color = Gray4_C5CAD2, + labelColor: Color = Gray3_9FA5AE, onTimeOut: (() -> Unit) = { }, textAlign: TextAlign = TextAlign.Left, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, @@ -437,7 +438,7 @@ fun DayoTimerTextField( errorContainerColor = Color.Transparent, focusedContainerColor = Color.Transparent, unfocusedLabelColor = Color.Transparent, // 라벨 - focusedLabelColor = Gray4_C5CAD2, + focusedLabelColor = Gray3_9FA5AE, errorLabelColor = Red_FF4545, focusedPlaceholderColor = Gray5_E8EAEE, // 힌트 unfocusedPlaceholderColor = Gray5_E8EAEE, From 20ac70f2a112a1458970d21aa111a0a9457562d6 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Mon, 20 Apr 2026 22:18:09 +0900 Subject: [PATCH 66/93] #1064 [QA] Fix folder option drop down menu --- .../daily/dayo/presentation/screen/folder/FolderScreen.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderScreen.kt index f95aa8685..68d617180 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderScreen.kt @@ -39,6 +39,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -410,7 +411,8 @@ private fun FolderDropdownMenu( onDismissRequest = { expanded.value = false }, modifier = Modifier .background(DayoTheme.colorScheme.background) - .width(140.dp) + .width(140.dp), + shape = RoundedCornerShape(16.dp) ) { menuItems.forEach { DropdownMenuItem( @@ -436,6 +438,9 @@ private fun FolderDropdownMenu( it.onClickMenu() expanded.value = false }, + modifier = Modifier + .padding(horizontal = 4.dp) + .clip(RoundedCornerShape(12.dp)), contentPadding = PaddingValues(horizontal = 16.dp, vertical = 11.5.dp) ) } From 5548b69d1aadce9f30a93e3d8f3043704d2ee68f Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Wed, 22 Apr 2026 20:55:37 +0900 Subject: [PATCH 67/93] #1055 [QA] Fix rounded corner on folder delete dialog --- .../java/daily/dayo/presentation/view/dialog/ConfirmDialog.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/dialog/ConfirmDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/dialog/ConfirmDialog.kt index 4fd215dd3..cb4f5a125 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/dialog/ConfirmDialog.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/dialog/ConfirmDialog.kt @@ -46,9 +46,9 @@ fun ConfirmDialog( modifier = modifier .background( DayoTheme.colorScheme.background, - RoundedCornerShape(10.dp) + RoundedCornerShape(16.dp) ) - .clip(RoundedCornerShape(10.dp)) + .clip(RoundedCornerShape(16.dp)) ) { Column { DialogHeader(title, description) From 0f25b05b8db253c0276397731cbd8d760818ec48 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Wed, 22 Apr 2026 20:56:15 +0900 Subject: [PATCH 68/93] #1055 [QA] Add spacer in confirm dialog --- .../java/daily/dayo/presentation/view/dialog/ConfirmDialog.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/dialog/ConfirmDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/dialog/ConfirmDialog.kt index cb4f5a125..8b9894b5d 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/dialog/ConfirmDialog.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/dialog/ConfirmDialog.kt @@ -5,7 +5,9 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Text @@ -87,6 +89,7 @@ private fun DialogHeader(title: String, description: String) { } if (description.isNotBlank()) { + Spacer(Modifier.height(8.dp)) Text( text = description, color = Gray2_767B83, From 0c62b26100ebcac5c2ffd9066de7d95739a3ad51 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Wed, 22 Apr 2026 20:57:41 +0900 Subject: [PATCH 69/93] Disable auto mirror img_default_folder_dayo_logo.xml --- .../src/main/res/drawable/img_default_folder_dayo_logo.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/res/drawable/img_default_folder_dayo_logo.xml b/presentation/src/main/res/drawable/img_default_folder_dayo_logo.xml index 8addc5687..a2b98185c 100644 --- a/presentation/src/main/res/drawable/img_default_folder_dayo_logo.xml +++ b/presentation/src/main/res/drawable/img_default_folder_dayo_logo.xml @@ -1,7 +1,7 @@ From 1341244404f7148dba7434cdbce81328754c030c Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Wed, 22 Apr 2026 20:58:14 +0900 Subject: [PATCH 70/93] Disable auto mirror ic_check_corner_round.xml --- presentation/src/main/res/drawable/ic_check_corner_round.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/res/drawable/ic_check_corner_round.xml b/presentation/src/main/res/drawable/ic_check_corner_round.xml index 4b9572185..96f06c515 100644 --- a/presentation/src/main/res/drawable/ic_check_corner_round.xml +++ b/presentation/src/main/res/drawable/ic_check_corner_round.xml @@ -1,7 +1,7 @@ @@ -12,4 +12,4 @@ android:strokeColor="#ffffff" android:strokeLineCap="round" android:strokeLineJoin="round" /> - \ No newline at end of file + From 163d9e06f3d5b9a24b04bd86b0ad97415235f0d6 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Wed, 22 Apr 2026 21:39:16 +0900 Subject: [PATCH 71/93] #1055 [QA] Add widthIn to confirm dialog --- .../daily/dayo/presentation/view/dialog/ConfirmDialog.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/dialog/ConfirmDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/dialog/ConfirmDialog.kt index 8b9894b5d..4634dc104 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/dialog/ConfirmDialog.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/dialog/ConfirmDialog.kt @@ -9,10 +9,10 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Text import androidx.compose.material3.Divider -import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -46,6 +46,7 @@ fun ConfirmDialog( ) { Box( modifier = modifier + .widthIn(min = 252.dp, max = 320.dp) .background( DayoTheme.colorScheme.background, RoundedCornerShape(16.dp) From 9bdf9a5c02193fc872018108e2c71b26d0b84313 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Mon, 27 Apr 2026 23:30:01 +0900 Subject: [PATCH 72/93] #1062 [QA] Remove bottom sheet style --- .../presentation/view/dialog/BottomSheetDialog.kt | 2 +- .../res/drawable/dialog_bottom_sheet_default.xml | 8 -------- presentation/src/main/res/values-night/themes.xml | 12 +----------- presentation/src/main/res/values/themes.xml | 14 +------------- 4 files changed, 3 insertions(+), 33 deletions(-) delete mode 100644 presentation/src/main/res/drawable/dialog_bottom_sheet_default.xml diff --git a/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt index c20c3a78e..d17a05ab1 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt @@ -68,7 +68,7 @@ fun BottomSheetDialog( modifier = Modifier .fillMaxWidth() .wrapContentHeight(), - shape = RoundedCornerShape(12.dp, 12.dp, 0.dp, 0.dp), + shape = RoundedCornerShape(20.dp, 20.dp, 0.dp, 0.dp), color = White_FFFFFF ) { Column( diff --git a/presentation/src/main/res/drawable/dialog_bottom_sheet_default.xml b/presentation/src/main/res/drawable/dialog_bottom_sheet_default.xml deleted file mode 100644 index c3b325661..000000000 --- a/presentation/src/main/res/drawable/dialog_bottom_sheet_default.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/values-night/themes.xml b/presentation/src/main/res/values-night/themes.xml index 136c45c19..0a88fee6c 100644 --- a/presentation/src/main/res/values-night/themes.xml +++ b/presentation/src/main/res/values-night/themes.xml @@ -12,8 +12,6 @@ #FFFFFFFF true - - @style/AppBottomSheetDialogTheme - - - - \ No newline at end of file + diff --git a/presentation/src/main/res/values/themes.xml b/presentation/src/main/res/values/themes.xml index 909bc9c74..5708138f3 100644 --- a/presentation/src/main/res/values/themes.xml +++ b/presentation/src/main/res/values/themes.xml @@ -12,8 +12,6 @@ #FFFFFFFF true - - @style/AppBottomSheetDialogTheme - - - - - \ No newline at end of file + From a04a17430895d1f3583ee1c59b5418190af022df Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Mon, 27 Apr 2026 23:31:21 +0900 Subject: [PATCH 73/93] #1062 [QA] Add bottom sheet menu height --- .../daily/dayo/presentation/view/dialog/BottomSheetDialog.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt index d17a05ab1..13bdd354d 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt @@ -160,21 +160,22 @@ fun BottomSheetDialog( Row( modifier = Modifier .fillMaxWidth() - .wrapContentHeight() + .height(56.dp) .background( if (isPressed) Gray6_F0F1F3 else White_FFFFFF, RoundedCornerShape(12.dp, 12.dp, 0.dp, 0.dp) ) - .padding(16.dp) .clickable( onClick = button.second, interactionSource = interactionSource, indication = null ), horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically ) { Text( text = button.first, + textAlign = TextAlign.Center, color = if ((isFirstButtonColored && index == 0) || (checkedButtonIndex == index)) checkedColor else normalColor, fontSize = 16.sp, style = DayoTheme.typography.b4 From 24cc8926d4f1f33f789344d243ba13e9be9104c8 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Mon, 27 Apr 2026 23:45:35 +0900 Subject: [PATCH 74/93] #1063 [QA] Update description text style --- .../src/main/java/daily/dayo/presentation/view/Comment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt b/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt index 98766de62..9949f00a3 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt @@ -115,12 +115,12 @@ fun CommentListView( Text( text = stringResource(id = R.string.post_comment_empty), - style = DayoTheme.typography.b3.copy(Gray3_9FA5AE), + style = DayoTheme.typography.b5.copy(Gray2_767B83), modifier = Modifier.padding(top = 12.dp, bottom = 2.dp) ) Text( text = stringResource(id = R.string.post_comment_empty_description), - style = DayoTheme.typography.caption1.copy(Gray4_C5CAD2) + style = DayoTheme.typography.caption4.copy(Gray3_9FA5AE) ) } } else { From e5a9a48789f86253371cdbc67af53abd8fdfd55d Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Mon, 27 Apr 2026 23:52:33 +0900 Subject: [PATCH 75/93] #1063 [QA] Add spacer to post comment empty state --- .../src/main/java/daily/dayo/presentation/view/Comment.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt b/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt index 9949f00a3..5eef5027b 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt @@ -118,6 +118,7 @@ fun CommentListView( style = DayoTheme.typography.b5.copy(Gray2_767B83), modifier = Modifier.padding(top = 12.dp, bottom = 2.dp) ) + Spacer(Modifier.height(2.dp)) Text( text = stringResource(id = R.string.post_comment_empty_description), style = DayoTheme.typography.caption4.copy(Gray3_9FA5AE) From 6ff4949ba225a5be7c3f81a159afb8fd94766c1e Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Mon, 27 Apr 2026 23:54:49 +0900 Subject: [PATCH 76/93] [QA] Update comment empty description string --- presentation/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 71e4e053a..3feac48d6 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -135,7 +135,7 @@ 개의 댓글 더보기 아직 댓글이 없어요 - 이 게시글에 대해 댓글을 남겨주세요 + 첫번째 댓글을 남겨보세요 게시물 수정 게시물 삭제 이 게시글을 정말 삭제할까요? From 8c52bd0e063d9145ba7f4ce0449dd599fbe8221e Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Tue, 28 Apr 2026 22:00:11 +0900 Subject: [PATCH 77/93] [QA] Replace height with heightIn --- .../daily/dayo/presentation/view/dialog/BottomSheetDialog.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt index 13bdd354d..67a511f07 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight @@ -160,7 +161,7 @@ fun BottomSheetDialog( Row( modifier = Modifier .fillMaxWidth() - .height(56.dp) + .heightIn(min = 56.dp) .background( if (isPressed) Gray6_F0F1F3 else White_FFFFFF, RoundedCornerShape(12.dp, 12.dp, 0.dp, 0.dp) From f8f6770ce8cd986c96e2bc8b4d46b2641de5f414 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Tue, 28 Apr 2026 22:10:37 +0900 Subject: [PATCH 78/93] [QA] Fix rounded corner shape --- .../daily/dayo/presentation/view/dialog/BottomSheetDialog.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt index 67a511f07..fa24d869e 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt @@ -164,7 +164,7 @@ fun BottomSheetDialog( .heightIn(min = 56.dp) .background( if (isPressed) Gray6_F0F1F3 else White_FFFFFF, - RoundedCornerShape(12.dp, 12.dp, 0.dp, 0.dp) + RoundedCornerShape(20.dp, 20.dp, 0.dp, 0.dp) ) .clickable( onClick = button.second, From 61e60360a6348c2b7f6dcd4d45a27379143bd668 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Tue, 28 Apr 2026 22:13:31 +0900 Subject: [PATCH 79/93] [QA] FIx comment mention list interaction and padding size --- .../daily/dayo/presentation/view/Comment.kt | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt b/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt index 5eef5027b..16443dbab 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -329,20 +330,24 @@ fun CommentMentionSearchView(userResults: LazyPagingItems, onClickFo modifier = Modifier .background(DayoTheme.colorScheme.background) .fillMaxWidth(), - contentPadding = PaddingValues(horizontal = 18.dp) + contentPadding = PaddingValues(start = 18.dp, end = 18.dp, top = 16.dp, bottom = 12.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) ) { items(userResults.itemCount) { index -> userResults[index]?.let { user -> + val interactionSource = remember { MutableInteractionSource() } + val isPressed = interactionSource.collectIsPressedAsState().value Row( modifier = Modifier - .background(DayoTheme.colorScheme.background) .fillMaxWidth() - .padding(vertical = 4.dp) + .clip(RoundedCornerShape(8.dp)) + .background(if (isPressed) Gray7_F6F6F7 else DayoTheme.colorScheme.background) .clickableSingle( - indication = ripple(bounded = false, radius = 8.dp, color = Gray7_F6F6F7), - interactionSource = remember { MutableInteractionSource() }, + indication = null, + interactionSource = interactionSource, onClick = { onClickFollowUser(user) } - ), + ) + .padding(horizontal = 8.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically ) { RoundImageView( @@ -350,12 +355,7 @@ fun CommentMentionSearchView(userResults: LazyPagingItems, onClickFo context = LocalContext.current, modifier = Modifier .clip(CircleShape) - .size(24.dp) - .clickableSingle( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onClick = { } - ), + .size(24.dp), imageDescription = "search users profile image", ) Spacer(modifier = Modifier.width(12.dp)) @@ -545,4 +545,4 @@ private fun PreviewCommentTextField() { focusRequester = commentFocusRequester, onClickPostComment = { } ) -} \ No newline at end of file +} From 1d4aa4d65cae1f8cd9dbc0ada4f2179253b3965a Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Tue, 28 Apr 2026 22:31:06 +0900 Subject: [PATCH 80/93] [QA] Fix comment delete snackbar display --- .../dayo/presentation/screen/post/PostScreen.kt | 12 ++++-------- .../view/dialog/CommentBottomSheetDialog.kt | 11 ++++------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/post/PostScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/post/PostScreen.kt index 8c4b27965..50fd3aa31 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/post/PostScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/post/PostScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState -import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateListOf @@ -106,12 +105,10 @@ fun PostScreen( postViewModel.requestDeletePostComment(commentId) } val postCommentDeleteSuccess by postViewModel.postCommentDeleteSuccess.observeAsState(Event(false)) - if (postCommentDeleteSuccess.getContentIfNotHandled() == true) { - postViewModel.requestPostComment(postId) - SideEffect { - coroutineScope.launch { - snackBarHostState.showSnackbar(context.getString(R.string.comment_delete_message)) - } + LaunchedEffect(postCommentDeleteSuccess) { + if (postCommentDeleteSuccess.getContentIfNotHandled() == true) { + postViewModel.requestPostComment(postId) + snackBarHostState.showSnackbar(context.getString(R.string.comment_delete_message)) } } var showReportDialog by remember { mutableStateOf(false) } @@ -493,4 +490,3 @@ private fun PreviewPostScreen() { ) } } - diff --git a/presentation/src/main/java/daily/dayo/presentation/view/dialog/CommentBottomSheetDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/dialog/CommentBottomSheetDialog.kt index 119f8970a..65f65b923 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/dialog/CommentBottomSheetDialog.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/dialog/CommentBottomSheetDialog.kt @@ -15,7 +15,6 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateListOf @@ -94,12 +93,10 @@ fun CommentBottomSheetDialog( postViewModel.requestDeletePostComment(commentId) } val postCommentDeleteSuccess by postViewModel.postCommentDeleteSuccess.observeAsState(Event(false)) - if (postCommentDeleteSuccess.getContentIfNotHandled() == true) { - postViewModel.requestPostComment(postId) - SideEffect { - coroutineScope.launch { - snackBarHostState.showSnackbar("댓글이 삭제되었어요.") - } + LaunchedEffect(postCommentDeleteSuccess) { + if (postCommentDeleteSuccess.getContentIfNotHandled() == true) { + postViewModel.requestPostComment(postId) + snackBarHostState.showSnackbar(context.getString(R.string.comment_delete_message)) } } var showReportDialog by remember { mutableStateOf(false) } From 15f37a5008f4d035c085274161bc5d8f0c82a1bb Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Tue, 28 Apr 2026 22:52:32 +0900 Subject: [PATCH 81/93] [QA] Fix comment bottom sheet header layout --- .../view/dialog/CommentBottomSheetDialog.kt | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/dialog/CommentBottomSheetDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/dialog/CommentBottomSheetDialog.kt index 65f65b923..50d5c7152 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/dialog/CommentBottomSheetDialog.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/dialog/CommentBottomSheetDialog.kt @@ -5,7 +5,9 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape @@ -92,7 +94,11 @@ fun CommentBottomSheetDialog( val onClickDelete: (Long) -> Unit = { commentId -> postViewModel.requestDeletePostComment(commentId) } - val postCommentDeleteSuccess by postViewModel.postCommentDeleteSuccess.observeAsState(Event(false)) + val postCommentDeleteSuccess by postViewModel.postCommentDeleteSuccess.observeAsState( + Event( + false + ) + ) LaunchedEffect(postCommentDeleteSuccess) { if (postCommentDeleteSuccess.getContentIfNotHandled() == true) { postViewModel.requestPostComment(postId) @@ -130,14 +136,24 @@ fun CommentBottomSheetDialog( } // create comment - val replyCommentState = remember { mutableStateOf?>(null) } // parent comment Id, reply comment + val replyCommentState = + remember { mutableStateOf?>(null) } // parent comment Id, reply comment val onClickPostComment: () -> Unit = { if (replyCommentState.value == null) { if (commentText.value.text.isNotBlank()) { - postViewModel.requestCreatePostComment(commentText.value.text, postId, mentionedMemberIds) + postViewModel.requestCreatePostComment( + commentText.value.text, + postId, + mentionedMemberIds + ) } } else { - postViewModel.requestCreatePostCommentReply(replyCommentState.value!!, commentText.value.text, postId, mentionedMemberIds) + postViewModel.requestCreatePostCommentReply( + replyCommentState.value!!, + commentText.value.text, + postId, + mentionedMemberIds + ) } } val onClickReply: (Pair?) -> Unit = { reply -> @@ -146,7 +162,8 @@ fun CommentBottomSheetDialog( // show mention user name val replyUsername = "@${replyCommentState.value?.second?.nickname} " - commentText.value = TextFieldValue(text = replyUsername, selection = TextRange(replyUsername.length)) + commentText.value = + TextFieldValue(text = replyUsername, selection = TextRange(replyUsername.length)) commentFocusRequester.requestFocus() } val commentEnabled = if (replyCommentState.value == null) { @@ -204,7 +221,7 @@ fun CommentBottomSheetDialog( Column( modifier = Modifier .fillMaxWidth() - .padding(top = 12.dp, bottom = 65.dp) + .padding(bottom = 65.dp) .wrapContentHeight(), ) { CommentBottomSheetDialogTitle(clearComment, onClickClose) @@ -222,8 +239,14 @@ fun CommentBottomSheetDialog( } Column(modifier = Modifier.align(Alignment.BottomCenter)) { - if (showMentionSearchView.value) CommentMentionSearchView(userResults, onClickFollowUser) - if (replyCommentState.value != null) CommentReplyDescriptionView(replyCommentState, onClickCancelReply) + if (showMentionSearchView.value) CommentMentionSearchView( + userResults, + onClickFollowUser + ) + if (replyCommentState.value != null) CommentReplyDescriptionView( + replyCommentState, + onClickCancelReply + ) CommentTextField( enabled = commentEnabled, commentText = commentText, @@ -258,12 +281,15 @@ private fun CommentBottomSheetDialogTitle(clearComment: () -> Unit, onClickClose Box( modifier = Modifier .fillMaxWidth() - .wrapContentHeight() + .height(48.dp) .background(DayoTheme.colorScheme.background) ) { Text( text = stringResource(id = R.string.comment), - modifier = Modifier.align(Alignment.Center), + modifier = Modifier + .align(Alignment.Center) + .padding(top = 15.dp, bottom = 6.dp) + .height(27.dp), textAlign = TextAlign.Center, style = DayoTheme.typography.b1.copy(color = Dark, fontWeight = FontWeight.SemiBold) ) @@ -275,7 +301,10 @@ private fun CommentBottomSheetDialogTitle(clearComment: () -> Unit, onClickClose }, iconContentDescription = "close", iconPainter = painterResource(id = R.drawable.ic_x), - iconButtonModifier = Modifier.align(Alignment.CenterEnd) + iconButtonModifier = Modifier + .align(Alignment.CenterEnd) + .padding(end = 8.dp) + .size(32.dp) ) } } From a120b2c1d0af9949f647bb6c1652d7dc38411c35 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Tue, 28 Apr 2026 23:09:59 +0900 Subject: [PATCH 82/93] [QA] Fix comment bottom sheet dismiss gesture --- .../main/java/daily/dayo/presentation/screen/main/MainScreen.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/main/MainScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/main/MainScreen.kt index 1241bd57d..05040e924 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/main/MainScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/main/MainScreen.kt @@ -292,6 +292,7 @@ internal fun MainScreen( onDismissRequest = { bottomSheetController.hide() }, modifier = Modifier.navigationBarsPadding(), sheetState = bottomSheetState, + sheetGesturesEnabled = false, dragHandle = null ) { Box { From 17c1f6c899f93508074d1ac5087ec9f727c71916 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Tue, 28 Apr 2026 23:14:04 +0900 Subject: [PATCH 83/93] [QA] Fix comment input height --- .../daily/dayo/presentation/view/Comment.kt | 49 +++++++++---------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt b/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt index 16443dbab..8b6c60d1e 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt @@ -12,25 +12,18 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Text -import androidx.compose.material.TextFieldDefaults -import androidx.compose.material.TextFieldDefaults.TextFieldDecorationBox -import androidx.compose.material.TextFieldDefaults.textFieldColors import androidx.compose.material3.Icon import androidx.compose.material3.ripple import androidx.compose.runtime.Composable @@ -56,7 +49,6 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview @@ -396,7 +388,6 @@ fun CommentReplyDescriptionView(replyCommentState: MutableState - TextFieldDecorationBox( - value = commentText.value.text, - innerTextField = innerTextField, - enabled = true, - singleLine = false, - visualTransformation = VisualTransformation.None, - interactionSource = interactionSource, - placeholder = { Text(text = "댓글을 남겨주세요", style = DayoTheme.typography.b6.copy(Gray4_C5CAD2)) }, - shape = DayoTheme.shapes.small.copy(all = CornerSize(12.dp)), - colors = textFieldColors(backgroundColor = Gray7_F6F6F7), - contentPadding = TextFieldDefaults.textFieldWithLabelPadding(top = 8.dp, bottom = 8.dp, start = 12.dp) - ) + Box( + modifier = Modifier + .fillMaxSize() + .background(Gray7_F6F6F7, RoundedCornerShape(12.dp)) + .padding(horizontal = 12.dp, vertical = 8.dp), + contentAlignment = Alignment.CenterStart + ) { + if (commentText.value.text.isEmpty()) { + Text( + text = "댓글을 남겨주세요", + style = DayoTheme.typography.b6.copy(Gray4_C5CAD2) + ) + } + innerTextField() + } } ) + Spacer(modifier = Modifier.width(8.dp)) + Box( modifier = Modifier - .defaultMinSize(minWidth = 64.dp, minHeight = 36.dp) + .height(36.dp) .clip(RoundedCornerShape(12.dp)) .background(color = if (enabled) Primary_23C882 else PrimaryL1_8FD9B9) .clickableSingle(enabled = enabled) { onClickPostComment() } From b5dab3274419ba5d2b9eb5a8ed51469e47850de9 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Tue, 28 Apr 2026 23:23:20 +0900 Subject: [PATCH 84/93] [QA] Fix Fong Weight --- .../java/daily/dayo/presentation/view/FeedPostView.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/FeedPostView.kt b/presentation/src/main/java/daily/dayo/presentation/view/FeedPostView.kt index 28d557dbc..6c9f2e4b5 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/FeedPostView.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/FeedPostView.kt @@ -285,19 +285,19 @@ fun FeedPostView( // like count val dec = DecimalFormat("#,###") Row(modifier = Modifier.weight(1f)) { - Text(text = stringResource(id = R.string.post_like_count_message_1), style = DayoTheme.typography.caption1.copy(Gray2_767B83)) + Text(text = stringResource(id = R.string.post_like_count_message_1), style = DayoTheme.typography.caption2.copy(Gray2_767B83)) Text( text = " ${dec.format(post.heartCount)} ", - style = DayoTheme.typography.caption1, + style = DayoTheme.typography.caption2, modifier = if (post.heartCount != 0) Modifier.clickableSingle { post.postId?.let { onPostLikeUsersClick(it) } } else Modifier, color = if (post.heartCount != 0) Primary_23C882 else Gray4_C5CAD2) - Text(text = stringResource(id = R.string.post_like_count_message_2), style = DayoTheme.typography.caption1.copy(Gray2_767B83)) + Text(text = stringResource(id = R.string.post_like_count_message_2), style = DayoTheme.typography.caption2.copy(Gray2_767B83)) } // comment count Row { - Text(text = " ${dec.format(post.commentCount)} ", style = DayoTheme.typography.caption1, color = if (post.commentCount != 0) Primary_23C882 else Gray4_C5CAD2) - Text(text = stringResource(id = R.string.post_comment_count_message), style = DayoTheme.typography.caption1.copy(Gray2_767B83)) + Text(text = " ${dec.format(post.commentCount)} ", style = DayoTheme.typography.caption2, color = if (post.commentCount != 0) Primary_23C882 else Gray4_C5CAD2) + Text(text = stringResource(id = R.string.post_comment_count_message), style = DayoTheme.typography.caption2.copy(Gray2_767B83)) } } From d4c80b00e998f5fa31cbdd8c3f8df2628e4e0cd2 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Tue, 28 Apr 2026 23:24:55 +0900 Subject: [PATCH 85/93] [QA] Fix Feed Empty View Image --- .../src/main/res/drawable/ic_feed_empty.xml | 105 +++++++++++------- 1 file changed, 63 insertions(+), 42 deletions(-) diff --git a/presentation/src/main/res/drawable/ic_feed_empty.xml b/presentation/src/main/res/drawable/ic_feed_empty.xml index 706bc7260..dc7c7c6b5 100644 --- a/presentation/src/main/res/drawable/ic_feed_empty.xml +++ b/presentation/src/main/res/drawable/ic_feed_empty.xml @@ -1,47 +1,68 @@ - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From d4b45d329292596b726375cf4686924cee3c3160 Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Tue, 28 Apr 2026 23:38:15 +0900 Subject: [PATCH 86/93] [QA] Fix comment empty state position --- .../daily/dayo/presentation/view/Comment.kt | 38 ++++++++++-------- .../view/dialog/CommentBottomSheetDialog.kt | 39 ++++++++++++++----- 2 files changed, 50 insertions(+), 27 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt b/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt index 8b6c60d1e..ee72340c7 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt @@ -91,31 +91,35 @@ fun CommentListView( if (postComments.isEmpty()) { Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, modifier = Modifier .background(DayoTheme.colorScheme.background) .fillMaxSize() - .padding(top = 12.dp, bottom = 30.dp) .then(modifier) ) { - if (showEmptyIcon) { - Icon( - painter = painterResource(id = R.drawable.ic_comment_empty), - contentDescription = "empty", - tint = Color.Unspecified + Spacer(modifier = Modifier.weight(64f)) + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + if (showEmptyIcon) { + Icon( + painter = painterResource(id = R.drawable.ic_comment_empty), + contentDescription = "empty", + tint = Color.Unspecified + ) + } + + Text( + text = stringResource(id = R.string.post_comment_empty), + style = DayoTheme.typography.b5.copy(Gray2_767B83), + modifier = Modifier.padding(top = 12.dp, bottom = 2.dp) + ) + Spacer(Modifier.height(2.dp)) + Text( + text = stringResource(id = R.string.post_comment_empty_description), + style = DayoTheme.typography.caption4.copy(Gray3_9FA5AE) ) } - Text( - text = stringResource(id = R.string.post_comment_empty), - style = DayoTheme.typography.b5.copy(Gray2_767B83), - modifier = Modifier.padding(top = 12.dp, bottom = 2.dp) - ) - Spacer(Modifier.height(2.dp)) - Text( - text = stringResource(id = R.string.post_comment_empty_description), - style = DayoTheme.typography.caption4.copy(Gray3_9FA5AE) - ) + Spacer(modifier = Modifier.weight(135f)) } } else { Column( diff --git a/presentation/src/main/java/daily/dayo/presentation/view/dialog/CommentBottomSheetDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/dialog/CommentBottomSheetDialog.kt index 50d5c7152..98aecd32b 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/dialog/CommentBottomSheetDialog.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/dialog/CommentBottomSheetDialog.kt @@ -325,16 +325,35 @@ private fun CommentBottomSheetDialogContent( .fillMaxHeight(0.8f) ) { item { - CommentListView( - currentMemberId = currentMemberId, - postComments = postComments, - onClickProfile = onClickCommentProfile, - onClickReply = onClickReply, - onClickDelete = onClickDelete, - onClickReport = onClickReport, - modifier = Modifier.padding(horizontal = 18.dp), - showEmptyIcon = true - ) + if (postComments.data.isEmpty()) { + Box( + modifier = Modifier + .fillParentMaxHeight() + .fillMaxWidth() + ) { + CommentListView( + currentMemberId = currentMemberId, + postComments = postComments, + onClickProfile = onClickCommentProfile, + onClickReply = onClickReply, + onClickDelete = onClickDelete, + onClickReport = onClickReport, + modifier = Modifier.padding(horizontal = 18.dp), + showEmptyIcon = true + ) + } + } else { + CommentListView( + currentMemberId = currentMemberId, + postComments = postComments, + onClickProfile = onClickCommentProfile, + onClickReply = onClickReply, + onClickDelete = onClickDelete, + onClickReport = onClickReport, + modifier = Modifier.padding(horizontal = 18.dp), + showEmptyIcon = true + ) + } } } } From 4be88c72e8e38ee9a5fa18ef0ef06b6b70efb43d Mon Sep 17 00:00:00 2001 From: DongJun Huh Date: Tue, 28 Apr 2026 23:47:40 +0900 Subject: [PATCH 87/93] [QA] Fix feed empty button style --- .../daily/dayo/presentation/screen/feed/FeedScreen.kt | 8 +++++++- .../main/java/daily/dayo/presentation/view/Button.kt | 10 ++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/feed/FeedScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/feed/FeedScreen.kt index b2ce5c71f..292abf8b2 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/feed/FeedScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/feed/FeedScreen.kt @@ -203,6 +203,12 @@ private fun FeedEmptyView(onEmptyViewClick: () -> Unit) { Text(text = stringResource(id = R.string.feed_empty_description), style = DayoTheme.typography.caption1.copy(Gray4_C5CAD2)) Spacer(modifier = Modifier.height(36.dp)) - FilledButton(onClick = onEmptyViewClick, label = stringResource(id = R.string.feed_empty_button)) + FilledButton( + onClick = onEmptyViewClick, + label = stringResource(id = R.string.feed_empty_button), + modifier = Modifier.height(44.dp), + contentPadding = PaddingValues(horizontal = 20.dp, vertical = 11.5.dp), + textStyle = DayoTheme.typography.b5 + ) } } diff --git a/presentation/src/main/java/daily/dayo/presentation/view/Button.kt b/presentation/src/main/java/daily/dayo/presentation/view/Button.kt index f13fa2a10..7679b6ba1 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/Button.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/Button.kt @@ -50,7 +50,9 @@ fun FilledButton( modifier: Modifier = Modifier, enabled: Boolean = true, isTonal: Boolean = false, - icon: @Composable (() -> Unit)? = null + icon: @Composable (() -> Unit)? = null, + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, + textStyle: TextStyle = DayoTheme.typography.b6 ) { val buttonColors = if (isTonal) ButtonDefaults.buttonColors( @@ -73,10 +75,10 @@ fun FilledButton( modifier = modifier, enabled = enabled, colors = buttonColors, - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), + contentPadding = contentPadding, content = { if (icon != null) icon() - Text(text = label, style = DayoTheme.typography.b6) + Text(text = label, style = textStyle) } ) } @@ -261,4 +263,4 @@ private fun PreviewDayoTextButton() { Text(text = "입니다.", style = DayoTheme.typography.caption3.copy(Gray4_C5CAD2)) } } -} \ No newline at end of file +} From 07f66887947a6d790acc3c93f82f5bb8e6a6d92a Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Wed, 29 Apr 2026 20:47:15 +0900 Subject: [PATCH 88/93] #1042 [QA] Update profile nickname text style --- .../dayo/presentation/screen/settings/SettingsScreen.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsScreen.kt index 808893d50..6dee5b312 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsScreen.kt @@ -42,8 +42,10 @@ import androidx.compose.ui.graphics.Color.Companion.White import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import daily.dayo.domain.model.Profile @@ -273,14 +275,16 @@ private fun SettingProfile( Text( text = profile?.nickname ?: "", color = Dark, - style = DayoTheme.typography.b2 + textAlign = TextAlign.Center, + style = DayoTheme.typography.b1 ) // email Text( text = profile?.email ?: "", color = Gray3_9FA5AE, - style = DayoTheme.typography.b6 + textAlign = TextAlign.Center, + style = DayoTheme.typography.b6.copy(lineHeight = 21.sp) ) Spacer(modifier = Modifier.height(16.dp)) From c440766de81c9d125de5c8f4bdb205e9e1e73f4b Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Wed, 29 Apr 2026 21:12:33 +0900 Subject: [PATCH 89/93] #1057 [refactor] Remove redundant nested Column --- .../screen/account/WithdrawScreen.kt | 59 +++++++++---------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt index 77250b29c..4dd0a24ef 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt @@ -455,33 +455,28 @@ fun WithdrawHoldBottomSheet( .fillMaxWidth() .wrapContentHeight() ) { - Column( - modifier = Modifier - .padding(bottom = 8.dp) - ) { - - Text( - text = stringResource(id = content.titleResId), - style = DayoTheme.typography.b1, - color = Dark, - ) + Text( + text = stringResource(id = content.titleResId), + style = DayoTheme.typography.b1, + color = Dark, + ) - val descriptionText = stringResource(id = content.descriptionResId) - if (descriptionText.isNotBlank()) { - Spacer( - modifier = Modifier.height( - if (isOtherReason) 2.dp else 4.dp - ) + val descriptionText = stringResource(id = content.descriptionResId) + if (descriptionText.isNotBlank()) { + Spacer( + modifier = Modifier.height( + if (isOtherReason) 2.dp else 4.dp ) - Text( - text = descriptionText, - style = DayoTheme.typography.caption2.copy( - color = Gray2_767B83, - fontWeight = FontWeight.Medium - ) + ) + Text( + text = descriptionText, + style = DayoTheme.typography.caption2.copy( + color = Gray2_767B83, + fontWeight = FontWeight.Medium ) - } + ) } + Spacer( modifier = Modifier.height( if (isOtherReason || hasWithdrawReasonGuide) 8.dp @@ -832,9 +827,11 @@ private fun WithdrawGuideContentUI( style = DayoTheme.typography.caption4, ) if (index != guideStrings.lastIndex) { - Spacer(modifier = Modifier - .width(6.dp) - .align(Alignment.CenterVertically)) + Spacer( + modifier = Modifier + .width(6.dp) + .align(Alignment.CenterVertically) + ) Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_arrow_right), contentDescription = null, @@ -843,9 +840,11 @@ private fun WithdrawGuideContentUI( .align(Alignment.CenterVertically), tint = Gray3_9FA5AE, ) - Spacer(modifier = Modifier - .width(6.dp) - .align(Alignment.CenterVertically)) + Spacer( + modifier = Modifier + .width(6.dp) + .align(Alignment.CenterVertically) + ) } } } @@ -933,4 +932,4 @@ data class WithdrawRetentionSheetContent( enum class WithdrawStep(val stepNum: Int) { REASON_SELECT(0), CONFIRM(1), -} \ No newline at end of file +} From 0d847e7a055e0adefbc6b0f4eddecd23c03abb44 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Wed, 29 Apr 2026 21:14:26 +0900 Subject: [PATCH 90/93] #1057 [fix] Add auto sizing to FilledRoundedCornerButton --- .../java/daily/dayo/presentation/view/Button.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/Button.kt b/presentation/src/main/java/daily/dayo/presentation/view/Button.kt index f13fa2a10..ed77a75b7 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/Button.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/Button.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.TextAutoSize import androidx.compose.material.IconButton import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add @@ -31,8 +32,10 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import daily.dayo.presentation.theme.Dark import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray2_767B83 @@ -116,7 +119,14 @@ fun FilledRoundedCornerButton( text = label, textAlign = TextAlign.Center, style = textStyle, - modifier = contentModifier ?: Modifier.fillMaxWidth() + modifier = contentModifier ?: Modifier.fillMaxWidth(), + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Clip, + autoSize = TextAutoSize.StepBased( + minFontSize = 12.sp, + maxFontSize = textStyle.fontSize + ) ) } }, @@ -261,4 +271,4 @@ private fun PreviewDayoTextButton() { Text(text = "입니다.", style = DayoTheme.typography.caption3.copy(Gray4_C5CAD2)) } } -} \ No newline at end of file +} From 50a946d0b339ab6d3f05df90f0fe37ed41674f66 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Wed, 29 Apr 2026 21:28:03 +0900 Subject: [PATCH 91/93] #1059 [QA] Update spacing in WithdrawConfirmScreen --- .../daily/dayo/presentation/screen/account/WithdrawScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt index 4dd0a24ef..9077f1a86 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt @@ -617,11 +617,11 @@ fun WithdrawConfirmScreen( fontWeight = FontWeight.SemiBold ), ) - Spacer(modifier = Modifier.height(20.dp)) + Spacer(modifier = Modifier.height(32.dp)) withdrawCheckLists.forEachIndexed { index, text -> WithdrawConfirmCheckItems(checkText = text) if (index != withdrawCheckLists.lastIndex) { - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(16.dp)) } } } From 68352df483564de73a64fede6768e4d19aa26673 Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Wed, 29 Apr 2026 21:38:16 +0900 Subject: [PATCH 92/93] #1058 [QA] Change check icon --- .../presentation/screen/account/WithdrawScreen.kt | 4 +++- .../presentation/view/dialog/BottomSheetDialog.kt | 5 ++++- presentation/src/main/res/drawable/ic_check.xml | 14 ++++++++------ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt index 9077f1a86..a5324dad0 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt @@ -687,11 +687,13 @@ fun WithdrawConfirmCheckItems( Row( modifier = Modifier .fillMaxWidth() - .wrapContentHeight() + .wrapContentHeight(), + verticalAlignment = Alignment.CenterVertically ) { Icon( painter = painterResource(id = R.drawable.ic_check), contentDescription = null, + modifier = Modifier.size(20.dp), tint = Primary_23C882, ) Spacer(modifier = Modifier.width(4.dp)) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt index fa24d869e..b53629ea9 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize @@ -152,7 +153,9 @@ fun BottomSheetDialog( Icon( imageVector = rightIcon, contentDescription = "", - modifier = Modifier.align(Alignment.CenterVertically), + modifier = Modifier + .size(24.dp) + .align(Alignment.CenterVertically), tint = Color.Unspecified ) } diff --git a/presentation/src/main/res/drawable/ic_check.xml b/presentation/src/main/res/drawable/ic_check.xml index 5da02f28b..94c281d9b 100644 --- a/presentation/src/main/res/drawable/ic_check.xml +++ b/presentation/src/main/res/drawable/ic_check.xml @@ -1,10 +1,12 @@ + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> + android:strokeLineCap="round" + android:strokeLineJoin="round" /> \ No newline at end of file From fd6fdd0b9e930bf2b4d01851cb820e363f5234ce Mon Sep 17 00:00:00 2001 From: Ju Yungyeom Date: Mon, 8 Jun 2026 22:13:42 +0900 Subject: [PATCH 93/93] [release] v.2.2.0 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 95bf3092e..0dc753281 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,8 +29,8 @@ android { applicationId "com.daily.dayo" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 21020 - versionName "2.1.2" + versionCode 22000 + versionName "2.2.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"