Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b6001a6
โœจ refactor(curation): ํ๋ ˆ์ด์…˜ ๋„๋ฉ”์ธ ์ฝ”๋“œ ์ •๋ฆฌ ๋ฐ ๋ฐ์ดํ„ฐ ํ๋ฆ„ ์ตœ์ ํ™”
KateteDeveloper Jun 21, 2026
b53bf4e
โœจ feat(curation): ํ๋ ˆ์ด์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ํ—ค๋” UI ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€
KateteDeveloper Jul 1, 2026
d769a9c
โœจ feat(design): ํ…Œ๋งˆ ์‹œ์Šคํ…œ ๋‚ด ํ๋ ˆ์ด์…˜ ์ „์šฉ ๊ทธ๋ผ๋ฐ์ด์…˜ ์†์„ฑ ์ถ”๊ฐ€
KateteDeveloper Jul 1, 2026
c8c23cf
โœจ feat(curation): ํ๋ ˆ์ด์…˜ ์นด๋“œ ์ƒ์„ธ ํ™”๋ฉด(`CurationCard1Screen`, `CurationCard3โ€ฆ
KateteDeveloper Jul 1, 2026
5d95ce0
๐Ÿ”ฅ refactor(curation): ํ๋ ˆ์ด์…˜ ์ƒ์„ธ ํ™”๋ฉด ๊ด€๋ จ ๋ ˆ๊ฑฐ์‹œ UI ์ปดํฌ๋„ŒํŠธ ์‚ญ์ œ
KateteDeveloper Jul 1, 2026
6c39254
โœจ refactor(design): TopBar ์ปดํฌ๋„ŒํŠธ ๋†’์ด ์ƒ์ˆ˜ ์กฐ์ • ๋ฐ ์ฃผ์„ ์ถ”๊ฐ€
KateteDeveloper Jul 1, 2026
4a0ddf7
๐Ÿ› fix(data): SocialProfileRequestDTO ์„ฑ๋ณ„ ๋งคํ•‘ ๋กœ์ง ์ˆ˜์ •
KateteDeveloper Jul 1, 2026
2a857cb
โœจ refactor(curation): `CalendarBox` ๋ฐ `CalendarIconBox` ํ…Œ๋งˆ ์ ์šฉ ๋ฐ ์ฝ”๋“œ ์ •๋ฆฌ
KateteDeveloper Jul 1, 2026
48f257f
โœจ feat(curation): ํ๋ ˆ์ด์…˜ ๊ฐ์ • ๋ถ„์„ UI ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€
KateteDeveloper Jul 1, 2026
84307e7
โœจ refactor(curation): ํ๋ ˆ์ด์…˜ ์นด๋“œ ์ปดํฌ๋„ŒํŠธ ๊ณ ๋„ํ™” ๋ฐ ํ…Œ๋งˆ ์‹œ์Šคํ…œ ์ ์šฉ
KateteDeveloper Jul 1, 2026
dba6325
โœจ feat(curation): ๋‚ด๋น„๊ฒŒ์ด์…˜ ๊ตฌ์กฐ ๊ฐœํŽธ ๋ฐ ์‹ ๊ทœ ์นด๋“œ ์ƒ์„ธ ํ™”๋ฉด ์—ฐ๊ฒฐ
KateteDeveloper Jul 1, 2026
2389b80
โœจ refactor(curation): ํ๋ ˆ์ด์…˜ ํ™”๋ฉด ๊ตฌ์กฐ ๊ฐœ์„  ๋ฐ ๋””์ž์ธ ์‹œ์Šคํ…œ ํ…Œ๋งˆ ์ ์šฉ
KateteDeveloper Jul 1, 2026
d9bdcb0
๐Ÿ’„ design(curation): ํ๋ ˆ์ด์…˜ ์นด๋“œ ๋ฐ ์บ˜๋ฆฐ๋” ์•„์ด์ฝ˜ UI ๋ ˆ์ด์•„์›ƒ ์„ธ๋ถ€ ์กฐ์ •
KateteDeveloper Jul 1, 2026
8b0b31a
Merge branch 'develop' into feature/#144-curation
KateteDeveloper Jul 1, 2026
8f36437
Update CurationCardItem.kt
KateteDeveloper Jul 1, 2026
aba9044
โœจ refactor(curation): UI ์ปดํฌ๋„ŒํŠธ ๋‚ด ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๊ธฐ๋ณธ๊ฐ’ ์ œ๊ฑฐ ๋ฐ ํŒŒ๋ผ๋ฏธํ„ฐ ํ•„์ˆ˜ํ™”
KateteDeveloper Jul 1, 2026
7196d32
โ™ป๏ธ refactor(curation): ์นด๋“œ ์ปดํฌ๋„ŒํŠธ ๋‚ด `imageUrl` ํƒ€์ž… ์•ˆ์ •์„ฑ ๊ฐ•ํ™” ๋ฐ ์ฝ”๋“œ ์ •๋ฆฌ
KateteDeveloper Jul 1, 2026
9e8ce20
โ™ป๏ธ refactor(curation): `CurationMainCardPager` ์ด๋ฏธ์ง€ URL ๋ฐ์ดํ„ฐ ํƒ€์ž… ๋ฐ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ฐœ์„ 
KateteDeveloper Jul 1, 2026
3ab76fa
โœจ refactor(curation): ๋ฉ”์ธ ์นด๋“œ ํŽ˜์ด์ € ์ด๋ฏธ์ง€ ์ดˆ๊ธฐ๊ฐ’ ๋ฐ ์ƒ์„ฑ ๋ฐฉ์‹ ๋ณ€๊ฒฝ
KateteDeveloper Jul 1, 2026
4ea0161
โ™ป๏ธ refactor(curation): CurationCardItem ๋‚ด ์ด๋ฏธ์ง€ URL ๋„ ์ฒดํฌ ๋กœ์ง ์ˆ˜์ •
KateteDeveloper Jul 2, 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
24 changes: 14 additions & 10 deletions app/src/main/java/com/linku/MainApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ fun MainApp(
}
}

// ๋‹‰๋„ค์ž„ ์ตœ์ƒ๋‹จ ๋’ค์น˜(์‚ฌ์šฉํ•˜๋Š” ์Šคํฌ๋ฆฐ)
val nickname by viewModel.nickname.collectAsStateWithLifecycle()


// ์•ฑ ์‹คํ–‰ ์‹œ ์‹คํ–‰ํ•˜์—ฌ ์ด์ „ ๊ณ„์ • ๊ธฐ๋ก ์‚ญ์ œ
// FIXME : ์ง€๋ฏผ๋‹˜ํ•œํ…Œ ์—ฌ์ญˆ์–ด๋ณด๊ธฐ. ๋งค๋ฒˆ ์•ฑ ์‹คํ–‰ํ•  ๋•Œ๋งˆ๋‹ค ์ตœ๊ทผ ๊ธฐ๋ก์„ ์ง€์šฐ๋Š” ๊ฒƒ๋ณด๋‹ค๋Š” ๋กœ๊ทธ์•„์›ƒ ๋•Œ ์ง€์šฐ๋Š”๊ฑด ์–ด๋–ค์ง€
LaunchedEffect(Unit) {
Expand Down Expand Up @@ -125,6 +129,14 @@ fun MainApp(
val navBackStackEntry by navigator.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route

LaunchedEffect(currentRoute) {
if (currentRoute == NavigationRoute.Home.route ||
currentRoute == "curation_list"
) {
viewModel.fetchNickname()
}
}

fun isTabRoute(current: String?, root: String): Boolean =
current == root || current?.startsWith("$root/") == true || current?.startsWith("$root?") == true

Expand Down Expand Up @@ -348,15 +360,6 @@ fun MainApp(
showNavBar = true
}

// ํ™ˆ ํƒญ ์ง„์ž… ๋ฐ ๋ณต๊ท€ ํ•  ๋•Œ๋งˆ๋‹ค ๋‹‰๋„ค์ž„ ๊ฐฑ์‹ 
LaunchedEffect(currentRoute) {
if (currentRoute == NavigationRoute.Home.route) {
viewModel.fetchNickname()
}
}

val nickname by viewModel.nickname.collectAsStateWithLifecycle()

HomeApp(
viewModel = homeViewModel,
nickname = nickname.orEmpty().ifBlank { "๋งํ" },
Expand Down Expand Up @@ -392,7 +395,8 @@ fun MainApp(
// ํ๋ ˆ์ด์…˜ ํŒŒํŠธ ๋ฆฌํŒฉํ† ๋ง ์ ์šฉ
curationGraph(
navigator = navigator,
showNavBar = { showNavBar = it }
showNavBar = { showNavBar = it },
nickname = nickname.orEmpty().ifBlank { "๋งํ" }
)


Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,6 @@
package com.linku.core.repository

import com.linku.core.model.CurationDetail
import com.linku.core.model.RecommendedLink


/** princeHw ์ž‘์—… ๊ณต๊ฐ„ */
interface CurationRepository {
//์ข‹์•„์š” ๊ธฐ๋Šฅ ์ž์ฒด ์‚ญ์ œ.
// ์ตœ๊ทผ ํ๋ ˆ์ด์…˜ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
// suspend fun getMyRecentCuration(userId: Long): CurationItem

//์ถ”์ฒœ(ํ๋ ˆ์ด์…˜ ๋””ํ…Œ์ผ)
suspend fun getRecommendedLinks(userId: Long, curationId: Long): List<RecommendedLink>

//์‚ฌ์šฉ์ž ๋””ํ…Œ์ผ ์ •๋ณด ์ œ๊ณต(ํ๋ ˆ์ด์…˜ ๋””ํ…Œ์ผ)
suspend fun getCurationDetail(curationId: Long): CurationDetail

//ํ๋ ˆ์ด์…˜ ์ถ”์ฒœ(๊ธฐ๋ณธ ํŽ˜์ด์ง€)
suspend fun getHomeRecommendedLinksTop2(userId: Long, curationId: Long): List<RecommendedLink>


}
35 changes: 1 addition & 34 deletions data/src/main/java/com/linku/data/api/CurationApi.kt
Original file line number Diff line number Diff line change
@@ -1,40 +1,7 @@
package com.linku.data.api

import com.linku.data.api.dto.BaseResponse
import com.linku.data.api.dto.server.CurationDetailResponse
import com.linku.data.api.dto.server.RecommendLinkItemDto
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
import retrofit2.Response
import okhttp3.ResponseBody

/** princeHw ์ž‘์—… ๊ณต๊ฐ„ */
interface CurationApi {

@GET("curations/latest/{userId}")
suspend fun getMyRecentCurationRaw(
@Path("userId") userId: Long
): Response<ResponseBody>


@GET("curations/recommend-links")
suspend fun getRecommendLinks(
@Query("userId") userId: Long,
@Query("curationId") curationId: Long
): BaseResponse<List<RecommendLinkItemDto>>

//ํ๋ ˆ์ด์…˜ ๋””ํ…Œ์ผ -> ํ˜„์žฌ ํ™”๋ฉด์—์‚ฌ ์—ฐ๋™ ๋บŒ. ui ์ˆ˜์ •์ด ์˜ˆ์ • ๋˜์–ด ์žˆ์Œ.
@GET("curations/detail/{curationId}")
suspend fun getCurationDetail(
@Path("curationId") curationId: Long
): BaseResponse<CurationDetailResponse>


//ํ๋ ˆ์ด์…˜ ๊ธฐ๋ณธ ํŽ˜์ด์ง€ ์ถ”์ฒœ
@GET("curations/recommend-links/internal/top2")
suspend fun getInternalTop2(
@Query("userId") userId: Long,
@Query("curationId") curationId: Long
): BaseResponse<List<RecommendLinkItemDto>>

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,25 @@ package com.linku.data.di.repository

import com.linku.core.repository.CurationRepository
import com.linku.data.api.CurationApi
import com.linku.data.api.ServerApi
import com.linku.data.implementation.repository.CurationRepositoryImpl
import com.linku.data.preference.AuthPreference
import com.squareup.moshi.Moshi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

/* ๋ฏธ๋ž˜์˜ ์ œ๊ฐ€ ์ˆ˜์ •ํ• ๊ฒ๋‹ˆ๋‹ค. */

@Module
@InstallIn(SingletonComponent::class)
object CurationRepositoryModule {


// @Provides
// @Singleton
// fun provideLinkuRepository(
// serverApi: ServerApi,
// authPreference: AuthPreference
// ): CurationRepository {
// return CurationRepositoryImpl(
// serverApi = serverApi,
// authPreference = authPreference
// )
// }
@Provides
@Singleton
fun provideCurationRepository( // ํ•จ์ˆ˜๋ช…๋„ ์˜๋ฏธ ๋งž๊ฒŒ ๋ณ€๊ฒฝ ๊ถŒ์žฅ
serverApi: ServerApi,
curationApi: CurationApi, // โ˜… ์ถ”๊ฐ€
authPreference: AuthPreference,
moshi: Moshi
fun provideCurationRepository(
curationApi: CurationApi,
): CurationRepository {
return CurationRepositoryImpl(
serverApi = serverApi,
curationApi = curationApi,
moshi = moshi
)
}
}
Original file line number Diff line number Diff line change
@@ -1,159 +1,14 @@
package com.linku.data.implementation.repository

import com.linku.core.model.CurationDetail
import com.linku.core.model.CurationItem
import com.linku.core.model.RecommendedLink
import com.linku.core.repository.CurationRepository
import com.linku.data.api.CurationApi
import com.linku.data.api.ServerApi
import com.linku.data.api.dto.server.CurationLatestResponse
import com.squareup.moshi.JsonReader
import com.squareup.moshi.Moshi
import okio.Buffer
import javax.inject.Inject


/* ์—ฌ๊ธฐ ์•„์ง ์•„๋ฌด๊ฒƒ๋„ ์—ฐ๋™ ์•ˆํ•ด์„œ ์ผ๋‹จ์€ ํŒจ์Šค. -๋ฏธ๋ž˜์˜ ๋‚˜์—๊ฒŒ ๋ถ€ํƒํ•˜๊ฒ ์Œ- */
/** princeHw ์ž‘์—… ๊ณต๊ฐ„ */
class CurationRepositoryImpl @Inject constructor(
private val serverApi: ServerApi,
private val curationApi: CurationApi,
private val moshi: Moshi,
private val curationApi: CurationApi
) : CurationRepository {


// override suspend fun getMyRecentCuration(userId: Long): CurationItem {
// Log.d("CurationRepo", "getMyRecentCuration() via RAW path") // โœ… ์ถ”๊ฐ€
// // โœ… Raw ํ˜ธ์ถœ
// val resp = curationApi.getMyRecentCurationRaw(userId)
// if (!resp.isSuccessful) throw HttpException(resp)
//
// val bodyStr = resp.body()?.string() ?: throw IllegalStateException("๋นˆ ์‘๋‹ต")
//
// Log.d("CurationApi", "response body = $bodyStr")
//
// // โœ… result ํ‚ค ์กด์žฌ ์—ฌ๋ถ€ ๋จผ์ € ํ™•์ธ
// return if (hasResultKey(bodyStr)) {
// val type = Types.newParameterizedType(
// BaseResponse::class.java,
// CurationLatestResponse::class.java
// )
// val adapter = moshi.adapter<BaseResponse<CurationLatestResponse>>(type)
// val base = adapter.fromJson(bodyStr)
// ?: throw IllegalStateException("ํŒŒ์‹ฑ ์‹คํŒจ(BaseResponse)")
// val dto = base.result // ํŒ€ ๊ทœ์น™์ƒ non-null
// CurationItem(
// id = dto.curationId,
// month = dto.month,
// thumbnailUrl = dto.thumbnailUrl
// )
// } else {
// val emptyAdapter = moshi.adapter(BaseEmptyResponse::class.java)
// val empty = emptyAdapter.fromJson(bodyStr)
// ?: throw IllegalStateException("ํŒŒ์‹ฑ ์‹คํŒจ(BaseEmptyResponse)")
// if (!empty.isSuccess) {
// throw IllegalStateException("์ตœ๊ทผ ํ๋ ˆ์ด์…˜ ํ˜ธ์ถœ ์‹คํŒจ: ${empty.code}/${empty.message}")
// }
// // ์ตœ์‹  ์—†์Œ โ†’ VM์—์„œ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ๋กœ ๋„˜๊ธฐ๊ธฐ
// throw NoSuchElementException("์ตœ๊ทผ ํ๋ ˆ์ด์…˜์ด ์—†์Šต๋‹ˆ๋‹ค.")
// }
// }

private fun hasResultKey(json: String): Boolean {
val reader = JsonReader.of(Buffer().writeUtf8(json))
if (reader.peek() != JsonReader.Token.BEGIN_OBJECT) return false
reader.beginObject()
var found = false
while (reader.hasNext()) {
val name = reader.nextName()
if (name == "result") { found = true; reader.skipValue(); break }
else reader.skipValue()
}
while (reader.hasNext()) reader.skipValue()
reader.endObject()
return found
}


private fun CurationLatestResponse.toDomain(): CurationItem {
return CurationItem(
id = this.curationId,
month = this.month,
thumbnailUrl = this.thumbnailUrl
)
}

override suspend fun getRecommendedLinks(
userId: Long,
curationId: Long
): List<RecommendedLink> {
val res = curationApi.getRecommendLinks(userId, curationId) // BaseResponse< List<...> >
if (!res.isSuccess) throw IllegalStateException(res.message ?: "์ถ”์ฒœ ๋งํฌ ํ˜ธ์ถœ ์‹คํŒจ")
val dtos = res.result ?: emptyList()

return dtos.mapNotNull { dto ->
val title = dto.title?.trim().orEmpty()
val url = dto.url?.trim().orEmpty()
if (title.isBlank() || url.isBlank()) return@mapNotNull null
val normalizedDomain = dto.domain
?.takeIf { it.isNotBlank() && it.lowercase() !in setOf("invalid", "unknown") }
?: runCatching { java.net.URL(url).host }.getOrNull()

RecommendedLink(
isInternal = dto.userLinkuId != null,
userLinkuId = dto.userLinkuId,
title = title,
url = url,
imageUrl = dto.imageUrl?.takeIf { it.isNotBlank() },
domain = normalizedDomain,
domainImageUrl = dto.domainImageUrl?.takeIf { it.isNotBlank() },
categories = dto.categories?.filter { it.isNotBlank() }
)
}.take(9)
}

//ํ๋ ˆ์ด์…˜ ๋””ํ…Œ์ผ ์‚ฌ์šฉ์ž ์ •๋ณด ์ œ๊ณต
override suspend fun getCurationDetail(curationId: Long): CurationDetail {
val res = curationApi.getCurationDetail(curationId) // BaseResponse<CurationDetailResponse>
if (!res.isSuccess) throw IllegalStateException(res.message ?: "ํ๋ ˆ์ด์…˜ ์ƒ์„ธ ์กฐํšŒ ์‹คํŒจ")
val dto = res.result
return CurationDetail(
curationId = dto.curationId,
month = dto.month,
topTags = dto.topTags.orEmpty().take(3),
headerMent = dto.headerMent,
footerMent = dto.footerMent
)
}


//ํ๋ ˆ์ด์…˜ ๊ธฐ๋ณธ ํŽ˜์ด์ง€ ์ถ”์ฒœ
override suspend fun getHomeRecommendedLinksTop2(
userId: Long, curationId: Long
): List<RecommendedLink> {
val res = curationApi.getInternalTop2(userId, curationId) // BaseResponse<List<...>>
if (!res.isSuccess) throw IllegalStateException(res.message ?: "ํ™ˆ ์ถ”์ฒœ ๋งํฌ ํ˜ธ์ถœ ์‹คํŒจ")
val dtos = res.result ?: emptyList()
return dtos.mapNotNull { dto ->
val title = dto.title?.trim().orEmpty()
val url = dto.url?.trim().orEmpty()
if (title.isBlank() || url.isBlank()) return@mapNotNull null
val normalizedDomain = dto.domain
?.takeIf { it.isNotBlank() && it.lowercase() !in setOf("invalid", "unknown") }
?: runCatching { java.net.URL(url).host }.getOrNull()

RecommendedLink(
isInternal = true,
userLinkuId = dto.userLinkuId,
title = title,
url = url,
imageUrl = dto.imageUrl?.takeIf { it.isNotBlank() },
domain = normalizedDomain,
domainImageUrl = dto.domainImageUrl?.takeIf { it.isNotBlank() },
categories = dto.categories?.filter { it.isNotBlank() }
)
}.take(2)
}



}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.linku.data.mapper

import com.linku.core.model.auth.*
import com.linku.core.model.auth.Gender
import com.linku.core.model.auth.Interest
import com.linku.core.model.auth.Job
import com.linku.core.model.auth.Purpose
import com.linku.data.api.dto.auth.signup.social.SocialProfileRequestDTO

object SocialProfileMapper {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,29 @@ sealed class ThemeColorScheme(
//๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ์ปฌ๋Ÿฌ์šฉ
val googleLoginColor: Color = Color(0xFF1F1F1F),
//๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ ๋ณด๋” ์ปฌ๋Ÿฌ์šฉ
val googleLoginBorderColor: Color = Color(0xFF747775)
val googleLoginBorderColor: Color = Color(0xFF747775),

//ํ๋ ˆ์ด์…˜ ์ „์šฉ ๊ทธ๋ผ๋ฐ์ด์…˜
val curationGradient: Brush = Brush.horizontalGradient(
listOf(Color(0xFF1451D5), Color(0xFF000208))
),

//ํ๋ ˆ์ด์…˜ ์บ˜๋ฆฐ๋” ๋ฐ•์Šค ์ปฌ๋Ÿฌ
val curationCalendarBoxColor: Color = Color(0xFFEFF4FF),

//ํ๋ ˆ์ด์…˜ ์นด๋“œ ๋ฐฐ๊ฒฝ (์ด๋ฏธ์ง€ ๋กœ๋”ฉ ์ „ fallback)
val curationCardBackground: Color = Color(0xFFF2F2F2),

//ํ๋ ˆ์ด์…˜ ์นด๋“œ ์ด๋ฏธ์ง€ ์œ„ ์˜ค๋ฒ„๋ ˆ์ด ๊ทธ๋ผ๋ฐ์ด์…˜
val curationCardOverlayGradient: Brush = Brush.verticalGradient(
colorStops = arrayOf(
0.0f to Color(0x00000000),
1.0f to Color(0x66000000)
)
),

//ํ๋ ˆ์ด์…˜ ๊ฐ์ • ๋ถ„์„ ์„น์…˜ ํƒ€์ดํ‹€ ๊ทธ๋ผ๋ฐ์ด์…˜
val emotionTitleGradient: Brush = Brush.horizontalGradient(
listOf(Color(0xFF2C6FFF), Color(0xFF000208))
),
)
Loading