-
Notifications
You must be signed in to change notification settings - Fork 3
[FEAT] 책 검색 화면 API 연결 #79
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
30e57f7
38357a0
e8abc49
ec08e2c
1b9f563
0224822
66c1e30
03163bf
1ed12fd
3cee27f
10c99be
911f282
a3b0ffe
394cfeb
b2667d6
c6bdff8
5a895c6
82bd113
2223337
97a2e26
bd8ef45
61f7a47
7159215
4e4e19c
52483ae
1ec924c
f19cb8c
d88b5bc
1cd08cf
6ce2ba2
a8f0863
198ea7d
0e57563
0851901
7042833
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.texthip.thip.data.model.book.request | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class BookSaveRequest( | ||
| @SerialName("type") val type: Boolean = false | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.texthip.thip.data.model.book.response | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class BookDetailResponse( | ||
| @SerialName("title") val title: String = "", | ||
| @SerialName("imageUrl") val imageUrl: String? = null, | ||
| @SerialName("authorName") val authorName: String = "", | ||
| @SerialName("publisher") val publisher: String = "", | ||
| @SerialName("isbn") val isbn: String = "", | ||
| @SerialName("description") val description: String = "", | ||
| @SerialName("recruitingRoomCount") val recruitingRoomCount: Int = 0, | ||
| @SerialName("readCount") val readCount: Int = 0, | ||
| @SerialName("isSaved") val isSaved: Boolean = false | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.texthip.thip.data.model.book.response | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class BookSaveResponse( | ||
| @SerialName("isbn") val isbn: String = "", | ||
| @SerialName("isSaved") val isSaved: Boolean = false | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package com.texthip.thip.data.model.book.response | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class BookSearchResponse( | ||
| @SerialName("searchResult") val searchResult: List<BookSearchItem> = emptyList(), | ||
| @SerialName("page") val page: Int = 0, | ||
| @SerialName("size") val size: Int = 0, | ||
| @SerialName("totalElements") val totalElements: Int = 0, | ||
| @SerialName("totalPages") val totalPages: Int = 0, | ||
| @SerialName("last") val last: Boolean = true, | ||
| @SerialName("first") val first: Boolean = true | ||
| ) | ||
|
|
||
| @Serializable | ||
| data class BookSearchItem( | ||
| @SerialName("title") val title: String = "", | ||
| @SerialName("imageUrl") val imageUrl: String? = null, | ||
| @SerialName("authorName") val authorName: String = "", | ||
| @SerialName("publisher") val publisher: String = "", | ||
| @SerialName("isbn") val isbn: String = "" | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.texthip.thip.data.model.book.response | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class MostSearchedBooksResponse( | ||
| @SerialName("bookList") val bookList: List<PopularBookItem> = emptyList() | ||
| ) | ||
|
|
||
| @Serializable | ||
| data class PopularBookItem( | ||
| @SerialName("rank") val rank: Int = 0, | ||
| @SerialName("title") val title: String = "", | ||
| @SerialName("imageUrl") val imageUrl: String? = null, | ||
| @SerialName("isbn") val isbn: String = "" | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package com.texthip.thip.data.model.book.response | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class RecentSearchResponse( | ||
| @SerialName("recentSearchList") val recentSearchList: List<RecentSearchItem> = emptyList() | ||
| ) | ||
|
|
||
| @Serializable | ||
| data class RecentSearchItem( | ||
| @SerialName("recentSearchId") val recentSearchId: Int = 0, | ||
| @SerialName("searchTerm") val searchTerm: String = "" | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package com.texthip.thip.data.model.book.response | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class RecruitingRoomsResponse( | ||
| @SerialName("recruitingRoomList") val recruitingRoomList: List<RecruitingRoomItem> = emptyList(), | ||
| @SerialName("totalRoomCount") val totalRoomCount: Int = 0, | ||
| @SerialName("nextCursor") val nextCursor: String? = null, | ||
| @SerialName("isLast") val isLast: Boolean = true | ||
| ) | ||
|
|
||
| @Serializable | ||
| data class RecruitingRoomItem( | ||
| @SerialName("roomId") val roomId: Int = 0, | ||
| @SerialName("bookImageUrl") val bookImageUrl: String? = null, | ||
| @SerialName("roomName") val roomName: String = "", | ||
| @SerialName("memberCount") val memberCount: Int = 0, | ||
| @SerialName("recruitCount") val recruitCount: Int = 0, | ||
| @SerialName("deadlineEndDate") val deadlineEndDate: String = "" | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,7 +18,7 @@ data class JoinedRoomListResponse( | |
| data class JoinedRoomResponse( | ||
| @SerialName("roomId") val roomId: Int, | ||
| @SerialName("bookImageUrl") val bookImageUrl: String?, | ||
| @SerialName("bookTitle") val bookTitle: String, | ||
| @SerialName("roomTitle") val roomTitle: String, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain서버 호환성(역직렬화) 위험: bookTitle → roomTitle 변경 시 구버전 응답과 불일치 가능 서버가 아직 "bookTitle"을 내려주는 경우 앱 크래시(누락 필드) 또는 역직렬화 실패가 발생할 수 있습니다. 구·신 규격을 모두 수용하도록 fallback을 추가하는 것을 권장합니다. 또한 안전성을 위해 기본값을 지정하면 런타임 오류를 줄일 수 있습니다. 권장 변경안: import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonNames
@Serializable
data class JoinedRoomResponse(
@SerialName("roomId") val roomId: Int,
@SerialName("bookImageUrl") val bookImageUrl: String?,
- @SerialName("roomTitle") val roomTitle: String,
+ @SerialName("roomTitle")
+ @JsonNames("bookTitle")
+ val roomTitle: String = "",
@SerialName("memberCount") val memberCount: Int,
@SerialName("userPercentage") val userPercentage: Int
)검증 스크립트(레포 내부에서 실행): 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "1) 코드베이스에서 남아있는 'bookTitle' 사용 위치 확인:"
rg -n --hidden --glob '!.git' --glob '!**/build/**' '\bbookTitle\b' -A 2
echo
echo "2) kotlinx-serialization 버전 확인(JsonNames 지원 여부 점검):"
rg -n --hidden --glob '!.git' --glob '!**/build/**' -e 'kotlinx-serialization' -e 'serialization' -e 'kotlinx.*serialization.*json' -A 2Length of output: 24376 group 응답 모델
• 파일: • 수정 예시(diff): import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonNames
@Serializable
data class JoinedRoomListResponse(
@SerialName("roomId") val roomId: Int,
@SerialName("bookImageUrl") val bookImageUrl: String?,
- @SerialName("roomTitle") val roomTitle: String,
+ @SerialName("roomTitle")
+ @JsonNames("bookTitle")
+ val roomTitle: String = "",
@SerialName("memberCount") val memberCount: Int,
@SerialName("userPercentage") val userPercentage: Int
)• 추가 확인 사항 |
||
| @SerialName("memberCount") val memberCount: Int, | ||
| @SerialName("userPercentage") val userPercentage: Int | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,14 @@ | ||
| package com.texthip.thip.data.repository | ||
|
|
||
| import com.texthip.thip.data.model.base.handleBaseResponse | ||
| import com.texthip.thip.data.model.book.request.BookSaveRequest | ||
| import com.texthip.thip.data.model.book.response.BookDetailResponse | ||
| import com.texthip.thip.data.model.book.response.BookSaveResponse | ||
| import com.texthip.thip.data.model.book.response.BookSavedResponse | ||
| import com.texthip.thip.data.model.book.response.BookSearchResponse | ||
| import com.texthip.thip.data.model.book.response.MostSearchedBooksResponse | ||
| import com.texthip.thip.data.model.book.response.RecentSearchResponse | ||
| import com.texthip.thip.data.model.book.response.RecruitingRoomsResponse | ||
| import com.texthip.thip.data.service.BookService | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
|
|
@@ -11,10 +19,59 @@ class BookRepository @Inject constructor( | |
| ) { | ||
|
|
||
| /** 저장된 책 또는 모임 책 목록 조회 */ | ||
| suspend fun getBooks(type: String) = runCatching { | ||
| suspend fun getBooks(type: String): Result<List<BookSavedResponse>> = runCatching { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기에서 BookSavedResponse를 List로 감싸기보다는 애초에 List로 감싸진걸 BookSavedResponse로 만드는건 어떤가요? |
||
| bookService.getBooks(type) | ||
| .handleBaseResponse() | ||
| .getOrThrow() | ||
| ?.bookList ?: emptyList() | ||
| } | ||
|
|
||
| /** 책 검색 */ | ||
| suspend fun searchBooks(keyword: String, page: Int = 1, isFinalized: Boolean = false): Result<BookSearchResponse?> = runCatching { | ||
| bookService.searchBooks(keyword, page, isFinalized) | ||
| .handleBaseResponse() | ||
| .getOrThrow() | ||
| } | ||
|
|
||
| /** 인기 책 조회 */ | ||
| suspend fun getMostSearchedBooks(): Result<MostSearchedBooksResponse?> = runCatching { | ||
| bookService.getMostSearchedBooks() | ||
| .handleBaseResponse() | ||
| .getOrThrow() | ||
| } | ||
|
|
||
| /** 최근 검색어 조회 */ | ||
| suspend fun getRecentSearches(type: String = "BOOK"): Result<RecentSearchResponse?> = runCatching { | ||
| bookService.getRecentSearches(type) | ||
| .handleBaseResponse() | ||
| .getOrThrow() | ||
| } | ||
|
|
||
| /** 최근 검색어 삭제 */ | ||
| suspend fun deleteRecentSearch(recentSearchId: Int): Result<Unit> = runCatching { | ||
| bookService.deleteRecentSearch(recentSearchId) | ||
| .handleBaseResponse() | ||
| .getOrThrow() | ||
| } | ||
|
|
||
| /** 책 상세 조회 */ | ||
| suspend fun getBookDetail(isbn: String): Result<BookDetailResponse?> = runCatching { | ||
| bookService.getBookDetail(isbn) | ||
| .handleBaseResponse() | ||
| .getOrThrow() | ||
| } | ||
|
|
||
| /** 책 저장/저장취소 */ | ||
| suspend fun saveBook(isbn: String, type: Boolean): Result<BookSaveResponse?> = runCatching { | ||
| bookService.saveBook(isbn, BookSaveRequest(type)) | ||
| .handleBaseResponse() | ||
| .getOrThrow() | ||
| } | ||
|
|
||
| /** 모집중인 방 조회 */ | ||
| suspend fun getRecruitingRooms(isbn: String, cursor: String? = null): Result<RecruitingRoomsResponse?> = runCatching { | ||
| bookService.getRecruitingRooms(isbn, cursor) | ||
| .handleBaseResponse() | ||
| .getOrThrow() | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,23 +65,26 @@ class GroupRepository @Inject constructor( | |
| suspend fun getRoomRecruiting(roomId: Int): Result<RoomRecruitingResponse> = runCatching { | ||
| groupService.getRoomRecruiting(roomId) | ||
| .handleBaseResponse() | ||
| .getOrThrow()!! | ||
| .getOrThrow() | ||
| ?: throw NoSuchElementException("모집중인 모임방 정보를 찾을 수 없습니다.") | ||
| } | ||
|
|
||
| /** 새 모임방 생성 */ | ||
| suspend fun createRoom(request: CreateRoomRequest): Result<Int> = runCatching { | ||
| groupService.createRoom(request) | ||
| val response = groupService.createRoom(request) | ||
| .handleBaseResponse() | ||
| .getOrThrow()!! | ||
| .roomId | ||
| .getOrThrow() | ||
| ?: throw NoSuchElementException("모임방 생성 응답을 받을 수 없습니다.") | ||
| response.roomId | ||
| } | ||
|
|
||
| /** 모임방 참여 또는 취소 */ | ||
| suspend fun joinOrCancelRoom(roomId: Int, type: String): Result<String> = runCatching { | ||
| val request = RoomJoinRequest(type = type) | ||
| groupService.joinOrCancelRoom(roomId, request) | ||
| val response = groupService.joinOrCancelRoom(roomId, request) | ||
| .handleBaseResponse() | ||
| .getOrThrow()!! | ||
| .type | ||
| .getOrThrow() | ||
| ?: throw NoSuchElementException("모임방 참여/취소 응답을 받을 수 없습니다.") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이거 .handleBaseResponse().getOrThrow()이렇게 수정부탁드립니둥 ~ |
||
| response.type | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
networkSecurityConfig 연결 자체는 OK. 다만 현재 설정(IP 기반)은 적용되지 않을 수 있으며, 빌드타입 분리 필요.
@xml/network_security_config에서 IP를 사용하는 domain-config는 매칭되지 않아 cleartext 허용 효과가 없을 가능성이 큽니다.검증을 위해 코드베이스에서 실제 사용 중인 엔드포인트를 확인해주세요.
다음 스크립트로 http 스킴 및 해당 IP 사용 여부를 검색할 수 있습니다:
릴리스 안전성 강화를 위해(선택):
릴리스에서 속성을 제거하는 변경 예시:
- android:networkSecurityConfig="@xml/network_security_config"🏁 Script executed:
Length of output: 54199
AndroidManifest 네트워크 보안 설정 빌드타입별 분리 권장
현 설정(IP 기반 domain-config)이 실제 엔드포인트와 매칭되지 않아 cleartext 허용 효과가 없으며, 코드베이스 전체에
http://스킴·해당 IP 참조도 발견되지 않았습니다. 빌드타입별로 설정을 분리해 명확히 관리하세요.<domain-config>에3.37.87.117만 명시 → Android의<domain>은 도메인 네임만 처리하므로 IP 주소 매칭 불가http://스킴·3.37.87.117직접 호출 없음조치 제안
app/src/main/AndroidManifest.xml에서android:networkSecurityConfig="@xml/network_security_config"제거src/debug/AndroidManifest.xml에만 추가app/src/main/res/xml/network_security_config.xml수정dev.example.com)을<domain>에 명시하거나<base-config cleartextTrafficPermitted="true"/>로 전역 cleartext 허용<base-config cleartextTrafficPermitted="false"/>유지<application android:usesCleartextTraffic="false">추가