[DIP/FE] 프로젝트 소개 & Kakao Oauth 구현

2026. 2. 23. 14:00·TAVE-16th

Ⅰ. 프로젝트 소개

1. 프로젝트 소개

단순히 장르별로 묶인 플레이리스트에 질리셨나요? DIP은 지금 당신이 위치한 공간의 '맥락'을 읽습니다.

  1. 위치를 읽다: 당신이 머무는 공간의 특성을 분석하여 장소의 감성을 극대화합니다.
  2. 소음을 분석하다: 다음으로 당신 주변의 데시벨을 측정합니다. 소음을 뚫고 들릴 강력한 비트 혹은 고요함을 채워줄 선율을 골라냅니다.
  3. 목표에 집중하다: 당신이 설정한 오늘의 목표에 맞춰 음악이 단순한 감상을 넘어 당신의 활동을 돕는 도구가 됩니다.

지금 바로 당신의 환경이 들려주는 음악에 귀를 기울여 보세요.

 

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를 발급받는 방식을 택했음.

  1. 사용자 로그인 시도: 앱에서 "카카오 로그인" 버튼 클릭.
  2. 카카오 토큰 발급: 카카오 SDK가 카카오 서버로부터 Access Token을 받아옴.
  3. 우리 서버로 토큰 전송: 앱은 받은 토큰을 sendKakaoTokenToBackend 함수를 통해 우리 백엔드로 전달함.
  4. 백엔드 검증: 서버는 카카오 서버에 토큰 유효 여부를 파악한 후, DB에 저장하거나 찾음.
  5. 서비스 전용 토큰 발급: 검증이 끝나면 백엔드에서 만든 우리 앱 전용 JWT(Access/Refresh)를 앱으로 돌려줌.
  6. 분기 처리: 기존 유저면 메인 화면으로, 신규 유저면 온보딩 화면으로 보냄.

 

2. 사전 설정

  1. *Kakao Developers 회원가입/로그인 진행 *
  2. 바로가기 -> https://developers.kakao.com
    안드로이드 -> https://developers.kakao.com/docs/latest/ko/kakaologin/android
  3. 내 어플리케이션 등록 및 안드로이드 패키지명과 키 해시 등록
  4. 리다이렉트 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. 카카오 공식 문서를 참고해 코드 작성




Ⅲ. 전체 코드

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
'TAVE-16th' 카테고리의 다른 글
  • [DIP/FE] 노래 추천받기(1) - 장소 선택, 데시벨 측정, 목표 설정
  • [DIP/FE] 온보딩 구현 - 문자열 검사 및 Spotify API로 아티스트 목록 가져오기
  • [TAVE 스터디 5주차] 메모 앱 만들기
  • [TAVE 스터디 4주차] 음악 목록 앱 만들기
choisio2
choisio2
sio2-dev 님의 블로그 입니다.
  • choisio2
    SiO2 for Developer
    choisio2
  • 전체
    오늘
    어제
    • 분류 전체보기 (46) N
      • TAVE-16th (14)
      • BDA-11th (16)
      • C++ (5)
      • 개인 프로젝트 (4)
      • 백준 (4) N
      • 컴퓨터 그래픽스 (1)
      • 잡담 (1)
  • 블로그 메뉴

    • 태그
    • 방명록
  • 링크

    • github.com/choisio2
  • 공지사항

  • 인기 글

  • 태그

    데이터분석모델링
    frontend
    AI시대
    viewpager2
    KakaoOauth
    바이브코딩
    C++
    calculator
    Tave
    BDA
    spotify
    백준
    polling
    데시벨측정
    kotlin
    코테
    개발자
    playconsole
    SpotifyAPI
    kotin
    프론트엔드
    알고리즘
    geminicli
    BDA #데이터분석모델링
    코딩테스트
    BDAI
    androidstudio
    알고리즘스터디
    개발자미래
    백준1463
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
choisio2
[DIP/FE] 프로젝트 소개 & Kakao Oauth 구현
상단으로

티스토리툴바