Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
71b0848
refactor: add meatball icon at Icons.kt
gdaegeun539 May 26, 2026
9256314
design: add submit arrow icon at Icons.kt
gdaegeun539 May 26, 2026
3d69c82
feat: Add CommentComponent and CommentInputField
gdaegeun539 May 26, 2026
41d9b2c
chore: update .gitignore
gdaegeun539 May 26, 2026
15790e9
fix: detekt issues
gdaegeun539 May 26, 2026
246ce71
feat: open id value at sample CommentComponentData
gdaegeun539 May 26, 2026
65162bb
feat: Add note detail screen and view model
gdaegeun539 May 26, 2026
85b8d15
feat: Make NoteComponent clickable
gdaegeun539 May 27, 2026
88f37a9
feat: Connect note detail screen navigation
gdaegeun539 May 27, 2026
b467161
fix: Change comment input's submit button color
gdaegeun539 May 27, 2026
2d2d3ac
fix: Resolve soft keyboard layout distortion in note detail screen
gdaegeun539 May 27, 2026
7d9691f
fix: Resolve navigation overlap for comment input
gdaegeun539 May 27, 2026
19a9039
docs/ai: Add suggest_commit antigravity workflow
gdaegeun539 May 27, 2026
3ce9d7e
docs/ai: Add create_pr opencode command
gdaegeun539 May 27, 2026
577f83f
refactor: Centralize nav padding in NoteDetail
gdaegeun539 May 28, 2026
4eb6eac
docs/ai: Remove prefix not in the commit rules
gdaegeun539 May 28, 2026
45a69ca
fix: Prevent send empty or whitespace-only comment
gdaegeun539 May 28, 2026
743d4b1
refactor: decouple CommentInputStatus to data
gdaegeun539 May 30, 2026
94524e2
feat: apply hilt at NoteDetailViewModel
gdaegeun539 May 30, 2026
ed890f9
fix: remove clearInteractionSource at CommentInput
gdaegeun539 May 30, 2026
7299988
fix: delete duplicate color at NoteDetailScreen
gdaegeun539 May 30, 2026
3ff2215
feat: add modifier at notedetail inner component
gdaegeun539 May 30, 2026
550545b
docs/ai: Remove prefix not in the commit rules
gdaegeun539 May 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions .agent/workflows/suggest_commit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
---
description: 스테이징된 변경을 기준으로 커밋 메시지를 제안하고 커밋은 실행하지 않음
---

스테이징된 변경만 기준으로 커밋 메시지를 생성하고 제안한다.
절대로 `git commit`을 실행하지 않는다.
staged 변경이 없으면 메시지 제안도 진행하지 않는다.

## Step 1: Gather Context

```bash
# What's staged?
git diff --cached --stat
git diff --cached --name-only

# What branch are we on?
git branch --show-current

# Recent commits for style reference
git log --oneline -5
```

언제나 staged 변경만 대상으로 보고, unstaged 변경이나 untracked 파일은 자동으로 포함하지 않는다.

## Step 2: Analyze Changes

staged diff를 읽고 변경의 성격을 파악한다.

```bash
git diff --cached
```

변경 유형은 아래 중 하나로 분류한다.

- init: 초기 설정
- feat: 신규 기능
- docs: 문서 변경
- build: 빌드 관련 변경
- design: 화면 디자인 변경
- fix: 버그 수정
- chore: 자잘한 유지보수
- refactor: 리팩터링
- ci: CI 설정 변경
- test: 테스트 변경

## Step 3: Generate Commit Message

기본 형식:

```text
<type>: <Subject>

<body - what and why, not how>
```

## Core rules

### 1. 커밋 메시지는 영문으로 쓴다

- type은 소문자 영어를 유지한다
- 제목과 본문 모두 영문으로 작성한다
- 고유명사나 코드 용어는 그대로 유지할 수 있다

### 2. 제목과 본문 사이는 한 줄 비운다

- 단순 변경이면 제목만 사용 가능
- 맥락이 필요하면 제목 + 빈 줄 + 본문 사용
- 제목 50자 안에 변경 의도를 충분히 담기 어렵다면 본문을 추가한다

### 3. 제목은 50자 이내를 우선한다

- 권장: 50자 이내
- 최대: 72자 이내

### 4. 제목은 명령형으로 쓴다

- Git 자체가 명령형을 사용하므로 일관성을 유지한다

### 5. 제목 끝에는 마침표를 붙이지 않는다

### 6. 본문은 72자 기준으로 줄바꿈한다

- Git은 자동 줄바꿈을 하지 않음
- 수동으로 72자마다 줄바꿈 필요

### 7. 본문은 how보다 what과 why를 설명한다

- 변경 이유와 배경 설명
- 이전 상태의 문제점 명시
- 코드 자체가 how를 설명하므로 맥락에 집중

## Step 4: Present Options

채팅에 커밋 메시지 후보 3개를 코드 블록으로 제안한다.
사용자가 바로 복사해서 쓸 수 있도록 형식을 유지한다.

```markdown
## Suggested Commits

### Option 1 (recommended)
```
feat: Add automatic token refresh

Refresh access tokens before session expiry so long-running usage
does not unexpectedly force the user to log in again.
```

### Option 2 (minimal)
```
feat: Add token refresh
```

### Option 3 (detailed)
```
feat: Introduce proactive JWT token refresh

Reduce unexpected session expiration during long-running usage by
renewing tokens before they expire.
```
```

## Final rule

- `git commit`을 실행하지 않는다
- 사용자가 원하더라도 이 명령 자체에서는 메시지 제안만 수행한다
- 실제 커밋이 필요하면 별도 커밋 워크플로우 또는 명령을 사용하도록 안내한다
- staged 변경만 기준으로 제안한다

# Reference article

@`.agent/rules/git-commit-rules.md`
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,8 @@ Temporary Items
# oh-my-openagent files
.sisyphus
.sisyphus/**
.omo
.omo/**

# antigravity cli files
.antigravitycli
Expand Down
124 changes: 124 additions & 0 deletions .opencode/command/create_pr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
---
description: GitHub CLI로 PR 제목과 본문을 준비하고 사용자 확인 후 PR 생성
---

현재 브랜치의 변경사항을 기준으로 PR 제목과 본문을 작성한다.
사용자 확인 전에는 절대로 `gh pr create`를 실행하지 않는다.
별도 base branch 지시가 없으면 `upstream/develop`으로 병합 요청을 생성한다.

## Step 1: Gather Context

아래 명령으로 PR 생성에 필요한 컨텍스트를 수집한다.

```bash
# GitHub CLI availability and auth
gh auth status

# Current branch and remotes
git branch --show-current
git remote -v

# Base branch freshness
git fetch upstream develop

# Current working tree and PR scope
git status --short
git log --oneline -5
git diff --stat upstream/develop...HEAD
git diff --name-only upstream/develop...HEAD
```

`gh auth status`가 실패하거나 `upstream` remote가 없으면 PR 생성을 진행하지 않고 사용자에게 안내한다.
별도 base branch 지시가 있으면 해당 base를 사용하되, 지시가 없으면 `upstream/develop`을 기준으로 한다.

## Step 2: Analyze PR Scope

아래 명령으로 변경 내용을 읽고 PR의 범위와 목적을 파악한다.

```bash
git diff upstream/develop...HEAD
```

PR 제목의 접두사는 커밋 메시지와 동일한 규칙을 따른다.

- init: 초기 설정
- feat: 신규 기능
- docs: 문서 변경
- build: 빌드 관련 변경
- design: 화면 디자인 변경
- fix: 버그 수정
- chore: 자잘한 유지보수
- refactor: 리팩터링
- ci: CI 설정 변경
- test: 테스트 변경

작업 범위가 모호하거나 복합적이면 `docs/ai`처럼 복합 접두사를 사용할 수 있다.
브랜치명에 Jira 티켓 번호가 포함되어 있으면 PR 제목의 접두사 앞에 티켓 번호를 작성한다.

예시:

```text
SCRUM-123 feat: Add social login
docs/ai: Add PR creation command
```

## Step 3: Draft PR Body

`.github/PULL_REQUEST_TEMPLATE.md`를 반드시 참조해 PR 본문을 작성한다.

```bash
cat .github/PULL_REQUEST_TEMPLATE.md
```

oh-my-openagent 플러그인을 사용할 수 있는 환경이라면 `writing` 카테고리의 서브에이전트가 본문 초안을 작성한다.
서브에이전트에는 아래 컨텍스트를 제공한다.

- PR template: `@.github/PULL_REQUEST_TEMPLATE.md`
- base branch: 기본 `upstream/develop`
- current branch
- changed file list
- diff summary
- relevant commit log

본문은 템플릿의 섹션을 유지하되, 불필요한 placeholder는 실제 내용으로 정리한다.
스크린샷이 필요하지 않은 변경이면 `ScreenShots` 섹션에는 해당 없음으로 명시한다.

## Step 4: Present Draft

바로 PR을 생성하지 말고 먼저 채팅에 PR 제목과 본문 초안을 제안한다.
사용자가 수정 요청을 하면 제목과 본문을 반영해 다시 제안한다.

## Step 5: Create PR on Confirmation

사용자가 PR 생성을 명시적으로 승인하면 본문을 임시 파일에 저장한 뒤 GitHub CLI로 PR을 생성한다.

```bash
gh pr create \
--repo project-lyrics/app-Android \
--base develop \
--head "$(git branch --show-current)" \
--title "<confirmed title>" \
--body-file "<prepared body file>"
```

fork 브랜치를 명시해야 하는 상황이면 `--head "gdaegeun539:$(git branch --show-current)"`처럼 owner를 포함한다.

PR 생성 후에는 결과를 확인한다.

```bash
gh pr view --json url,title,state,baseRefName,headRefName
```

## Final rule

- 사용자 확인 없이 `gh pr create`를 실행하지 않는다
- 별도 base branch 지시가 없으면 `upstream/develop`을 사용한다
- PR 본문은 `.github/PULL_REQUEST_TEMPLATE.md`를 기반으로 작성한다
- PR 본문 초안은 가능한 경우 `writing` 카테고리 서브에이전트가 작성한다
- PR 제목은 Jira ticket + commit prefix 규칙을 따른다
- `gh auth status` 또는 `upstream` remote 확인에 실패하면 PR 생성을 중단한다

# Reference article

@`.agent/rules/git-commit-rules.md`
@`.github/PULL_REQUEST_TEMPLATE.md`
1 change: 0 additions & 1 deletion .opencode/command/suggest_commit.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ git diff --cached
- refactor: 리팩터링
- ci: CI 설정 변경
- test: 테스트 변경
- perf: 성능 개선

## Step 3: Generate Commit Message

Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:windowSoftInputMode="stateUnspecified|adjustNothing"
Comment thread
gdaegeun539 marked this conversation as resolved.
Comment thread
gdaegeun539 marked this conversation as resolved.
Comment thread
gdaegeun539 marked this conversation as resolved.
android:theme="@style/Theme.Feelin.Starting">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,11 @@ val FindSquareIcon: ImageVector
val RefreshIcon: ImageVector
@Composable
get() = ImageVector.vectorResource(id = R.drawable.refresh)

val MeatballIcon: ImageVector
@Composable
get() = ImageVector.vectorResource(id = R.drawable.meatball_light)
Comment on lines +116 to +118

val SubmitArrowIcon: ImageVector
@Composable
get() = ImageVector.vectorResource(id = R.drawable.submit_arrow)
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ sealed class FeelinDestination(
) {
companion object {
private const val LOGIN_ROUTE = "login"
private const val NOTE_DETAIL_ROUTE = "note_detail"
private const val NOTE_ID_ARGUMENT = "noteId"
private const val WEB_VIEW_URL_ARGUMENT = "url"
}

Expand Down Expand Up @@ -37,6 +39,13 @@ sealed class FeelinDestination(
object Home : FeelinDestination(route = "home")
object NoteSearch : FeelinDestination(route = "note_search")
object NoteSearchResult : FeelinDestination(route = "note_search_result")
object NoteDetail : FeelinDestination(route = "$NOTE_DETAIL_ROUTE/{$NOTE_ID_ARGUMENT}") {
const val NoteIdArgument = NOTE_ID_ARGUMENT

fun createRoute(noteId: Long): String {
return "$NOTE_DETAIL_ROUTE/$noteId"
}
}
object MyPage : FeelinDestination(route = "my_page")
object Setting : FeelinDestination(route = "setting")
object UserInfo : FeelinDestination(route = "user_info")
Expand Down
33 changes: 31 additions & 2 deletions app/src/main/java/com/lyrics/feelin/navigation/FeelinNavHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import com.lyrics.feelin.presentation.view.mypage.MyPageScreen
import com.lyrics.feelin.presentation.view.mypage.MyPageViewModel
import com.lyrics.feelin.presentation.view.mypage.setting.SettingScreen
import com.lyrics.feelin.presentation.view.mypage.userinfo.UserInfoScreen
import com.lyrics.feelin.presentation.view.note.detail.NoteDetailScreen
import com.lyrics.feelin.presentation.view.note.search.NoteSearchScreen
import com.lyrics.feelin.presentation.view.note.search.result.NoteSearchResultScreen
import com.lyrics.feelin.presentation.view.onboarding.OnboardingUiState
Expand Down Expand Up @@ -265,7 +266,13 @@ private fun NavGraphBuilder.mainNavGraph(navController: NavHostController) {
navigation(startDestination = FeelinDestination.Home.route, route = FeelinDestination.HomeGraph.route) {
composable(FeelinDestination.Home.route) {
MainScaffold(navController = navController, selectedIndex = 0) {
CommunityMainScreen("필릭스", onBack = {})
CommunityMainScreen(
artistName = "필릭스",
onBack = {},
onNoteClick = { noteId ->
navController.navigate(FeelinDestination.NoteDetail.createRoute(noteId))
},
)
}
}
}
Expand All @@ -286,11 +293,30 @@ private fun NavGraphBuilder.mainNavGraph(navController: NavHostController) {

composable(FeelinDestination.NoteSearchResult.route) {
MainScaffold(navController = navController, selectedIndex = 1) {
NoteSearchResultScreen(onBackClick = { navController.popBackStack() })
NoteSearchResultScreen(
onBackClick = { navController.popBackStack() },
onNoteClick = { noteId ->
navController.navigate(FeelinDestination.NoteDetail.createRoute(noteId))
},
)
}
}
}

composable(
route = FeelinDestination.NoteDetail.route,
arguments = listOf(navArgument(FeelinDestination.NoteDetail.NoteIdArgument) { type = NavType.LongType }),
) { backStackEntry ->
val noteId = backStackEntry.arguments
?.getLong(FeelinDestination.NoteDetail.NoteIdArgument)
?: return@composable

NoteDetailScreen(
noteId = noteId,
onBackClick = { navController.popBackStack() },
)
}

myPageNavGraph(navController)
}
}
Expand All @@ -306,6 +332,9 @@ private fun NavGraphBuilder.myPageNavGraph(navController: NavHostController) {
MainScaffold(navController = navController, selectedIndex = 2) {
MyPageScreen(
onSettingClick = { navController.navigate(FeelinDestination.Setting.route) },
onNoteClick = { noteId ->
navController.navigate(FeelinDestination.NoteDetail.createRoute(noteId))
},
viewModel = viewModel,
)
}
Expand Down
Loading
Loading