Ⅰ. 프로젝트 소개
1. 프로젝트 소개
단순히 장르별로 묶인 플레이리스트에 질리셨나요? DIP은 지금 당신이 위치한 공간의 '맥락'을 읽습니다.
- 위치를 읽다: 당신이 머무는 공간의 특성을 분석하여 장소의 감성을 극대화합니다.
- 소음을 분석하다: 다음으로 당신 주변의 데시벨을 측정합니다. 소음을 뚫고 들릴 강력한 비트 혹은 고요함을 채워줄 선율을 골라냅니다.
- 목표에 집중하다: 당신이 설정한 오늘의 목표에 맞춰 음악이 단순한 감상을 넘어 당신의 활동을 돕는 도구가 됩니다.
지금 바로 당신의 환경이 들려주는 음악에 귀를 기울여 보세요.
2. 주요 기능
- 맥락 기반 추천: 사용자의 위치, 주변 소음, 목표를 기반으로 Agent가 사용자 맞춤형 플레이리스트 생성
- 실시간 분석: 실시간 주변 소음 데시벨 측정 및 현재 위치와 목표 설정
- Spotify 연동: 생성된 플레이리스트를 Spotify 앱으로 바로 연결 (Deep Link)
- 히스토리 관리: Room DB를 활용하여 최근 추천받은 몰입 테마 기록 저장 및 관리
- 소셜 로그인: Kakao OAuth를 이용한 간편 로그인 및 사용자 인증
3. 정보구조도(IA)

4. 프로젝트 구조
com.mobile.soundscape
├── api
│ ├── apis/ # Retrofit API 인터페이스 정의
│ ├── client/ # Retrofit, OkHttp 클라이언트 설정
│ └── dto/ # 서버 통신용 DTO
│
├── data/ # Repository/Token 관리 Manager, 공용 데이터 정의, 로컬 DB
├── home/ # 홈, 라이브러리, 마이페이지
├── explore/ # 둘러보기 화면
├── recommendation/ # 장소, 데시벨, 목표 설정 화면
├── result/ # 추천 결과 화면
├── evaluation/ # 사용자 평가 화면
├── onboarding/ # 온보딩(이름 설정, 아티스트 및 장르 취향 설정)
├── login/ # 로그인
│
├── MainActivity.kt # 메인 Activity
└── SoundscapeApp.kt # Application 클래스
5. Flow Chart

Ⅱ. Kakao Oauth 구현
1. 카카오 로그인 논리 & 순서 (Flow)
우리 앱은 카카오 토큰을 그대로 쓰지 않고, 우리 서버의 자체 JWT를 발급받는 방식을 택했음.
- 사용자 로그인 시도: 앱에서 "카카오 로그인" 버튼 클릭.
- 카카오 토큰 발급: 카카오 SDK가 카카오 서버로부터 Access Token을 받아옴.
- 우리 서버로 토큰 전송: 앱은 받은 토큰을 sendKakaoTokenToBackend 함수를 통해 우리 백엔드로 전달함.
- 백엔드 검증: 서버는 카카오 서버에 토큰 유효 여부를 파악한 후, DB에 저장하거나 찾음.
- 서비스 전용 토큰 발급: 검증이 끝나면 백엔드에서 만든 우리 앱 전용 JWT(Access/Refresh)를 앱으로 돌려줌.
- 분기 처리: 기존 유저면 메인 화면으로, 신규 유저면 온보딩 화면으로 보냄.
2. 사전 설정
- *Kakao Developers 회원가입/로그인 진행 *
- 바로가기 -> https://developers.kakao.com
안드로이드 -> https://developers.kakao.com/docs/latest/ko/kakaologin/android - 내 어플리케이션 등록 및 안드로이드 패키지명과 키 해시 등록
- 리다이렉트 URI 설정
인가 코드를 받기 위해 AndroidManifest.xml에 액티비티 설정을 추가해야 함. 이거 빼먹으면 로그인 성공 후 내 앱으로 못 돌아옴.
<activity
android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- 리다이렉트 URI: "kakao${NATIVE_APP_KEY}://oauth" -->
<data android:host="oauth"
android:scheme="kakao${NATIVE_APP_KEY}" />
</intent-filter>
</activity>
- 카카오 공식 문서를 참고해 코드 작성

Ⅲ. 전체 코드
1. 초기화 및 클릭 시 로그인 시작
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
// 디버깅을 위한 키 해시 확인
var keyHash = Utility.getKeyHash(this)
binding.btnKakaoOauth.setOnClickListener {
startKakaoLogin() // 로그인 로직 시작
}
}
2. 카카오 SDK 로그인 호출
// 카카오 로그인하고 온보딩 한 이력있으면 홈으로
private fun startKakaoLogin() {
// 로그인 공통 콜백 (카카오톡으로 하든, 웹으로 하든 결과는 여기로 옴)
val callback: (OAuthToken?, Throwable?) -> Unit = { token, error ->
if (error != null) {
Toast.makeText(this, "카카오 로그인에 실패했습니다.", Toast.LENGTH_SHORT).show()
} else if (token != null) {
// 카카오에서 받은 토큰을 백엔드로 전송!
sendKakaoTokenToBackend(token.accessToken)
}
}
// 카카오톡 앱이 설치되어 있으면 카카오톡으로 로그인
if (UserApiClient.instance.isKakaoTalkLoginAvailable(this)) {
UserApiClient.instance.loginWithKakaoTalk(this) { token, error ->
if (error != null) {
// 사용자가 '취소'를 누른 경우엔 웹 로그인을 시도하지 않고 종료
if (error is ClientError && error.reason == ClientErrorCause.Cancelled) {
return@loginWithKakaoTalk
}
// 카카오톡 로그인 실패 시(설치 안 됨 등), 웹(계정)으로 로그인 시도
UserApiClient.instance.loginWithKakaoAccount(this, callback = callback)
} else if (token != null) {
// 카카오에서 받은 토큰을 백엔드로 전송!
sendKakaoTokenToBackend(token.accessToken)
}
}
} else {
// 카카오톡 없으면 바로 웹으로 로그인 시도
UserApiClient.instance.loginWithKakaoAccount(this, callback = callback)
}
}
3. 백엔드에게 토큰 전송
// 백엔드 서버에 토큰 전송
private fun sendKakaoTokenToBackend(kakaoAccessToken: String) {
val request = LoginRequest(kakaoAccessToken = kakaoAccessToken)
RetrofitClient.loginApi.loginKakao(request).enqueue(object : Callback<BaseResponse<LoginResponse>> {
override fun onResponse(
call: Call<BaseResponse<LoginResponse>>,
response: Response<BaseResponse<LoginResponse>>
) {
if (response.isSuccessful) {
val body = response.body()
// 백엔드 로직 성공 (SUCCESS)
if (body != null && body.result == "SUCCESS") {
val loginData = body.data
if (loginData != null) {
// 백엔드가 준 JWT 토큰을 내부 저장소에 보관
TokenManager.saveToken(
context = applicationContext,
accessToken = loginData.accessToken,
refreshToken = loginData.refreshToken
)
handleLoginSuccess(loginData.isOnboarded)
}
} else {
// 통신은 됐지만 비즈니스 로직 실패
val errorMsg = body?.message ?: "알 수 없는 서버 오류"
Toast.makeText(this@LoginActivity, "로그인 실패: $errorMsg\n 잠시 후 다시 시도해주세요.", Toast.LENGTH_SHORT).show()
}
} else {
// HTTP 400~500 에러
val errorBody = response.errorBody()?.string()
Toast.makeText(this@LoginActivity, "서버 연결 실패: $errorBody\n 잠시 후 다시 시도해주세요.", Toast.LENGTH_SHORT).show()
}
}
override fun onFailure(call: Call<BaseResponse<LoginResponse>>, t: Throwable) {
Toast.makeText(this@LoginActivity, "네트워크 연결을 확인해주세요.", Toast.LENGTH_SHORT).show()
}
})
}
[서버 통신 interface/dto]
interface LoginApi {
// 백엔드로 code 보내서 -> 액세스 토큰 발급받기
@POST("api/v1/auth/login")
fun loginKakao(
@Body request: LoginRequest
): Call<BaseResponse<LoginResponse>>
}
data class LoginResponse(
// 엑세스 토큰
@SerializedName("accessToken")
val accessToken: String,
// 백엔드가 발급한 JWT Refresh Token (이게 있어야 로그인이 안 풀림)
@SerializedName("refreshToken")
val refreshToken: String,
@SerializedName("isOnboarded")
val isOnboarded: Boolean
)
data class LoginRequest(
@SerializedName("kakaoAccessToken")
val kakaoAccessToken: String
)
4. (추가) 로그인 후 분기 처리
업데이트 완료 여부를 확인하기 위해서 추가한 분기 처리 로직
// 로그인 성공 후 분기 처리
private fun handleLoginSuccess(serverIsOnboarded: Boolean) {
// 로컬에 저장된 v2 완료 여부 확인
val localOnboardingDone = PreferenceManager.isOnboardingFinished(this)
// 조건 1. 로컬에 v2 키가 있고 & 서버에서도 온보딩 완료면 -> 메인으로 통과
if (localOnboardingDone && serverIsOnboarded) {
val intent = Intent(this, MainActivity::class.java)
// 로그인 화면이 백스택에 남지 않게 클리어
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
finish()
}
else {
// [그 외 모든 경우] -> 온보딩 진행!
// case 1: 신규 유저 (local=false, server=false)
// case 2: 업데이트 유저 (local=false, server=true) -> 강제 재온보딩
// 프래그먼트 교체 (온보딩 시작)
val fragment = SetnameFragment()
supportFragmentManager.beginTransaction()
.replace(R.id.onboarding_fragment_container, fragment)
.commit()
}
}
}
Ⅳ. 실행화면 UI
![]() |
![]() |
|---|
👾👉 DIP Android Github: https://github.com/choisio2/DIP_android
GitHub - choisio2/DIP_android: Tave 16th team 리듬타고가는중 - 후반기 프로젝트 DIP (위치와 소음 데이터를
Tave 16th team 리듬타고가는중 - 후반기 프로젝트 DIP (위치와 소음 데이터를 분석해 맥락 기반 음악을 추천하는 안드로이드 앱) - choisio2/DIP_android
github.com
📽️👉 풀시연영상 보러가기: https://youtu.be/HjFp9eipC_4
'TAVE-16th' 카테고리의 다른 글
| [DIP/FE] 노래 추천받기(1) - 장소 선택, 데시벨 측정, 목표 설정 (0) | 2026.03.07 |
|---|---|
| [DIP/FE] 온보딩 구현 - 문자열 검사 및 Spotify API로 아티스트 목록 가져오기 (0) | 2026.03.07 |
| [TAVE 스터디 5주차] 메모 앱 만들기 (0) | 2026.01.04 |
| [TAVE 스터디 4주차] 음악 목록 앱 만들기 (0) | 2026.01.04 |
| [TAVE 스터디 4주차] ListView 만들기 & 더블클릭 종료 (0) | 2026.01.04 |


