- DB 버전 업그레이드 불필요: 앱 미출시로 기존 테이블 수정 없이 진행
- 그룹-기프티콘 관계 변경: 기프티콘에 group_id가 아닌, 그룹이 gifticonIds 배열을 가지는 구조
- 다대다 관계: 한 기프티콘을 여러 그룹에 공유 가능
- 서버 중심 설계: group_id나 gifticon_id로 데이터 조회하는 구조
WiFi, 멤버십, 기프티콘을 하나의 테이블로 통합:
- 공통점: 모두 QR/바코드 기반 공유 아이템
- Type 필드: "GIFTICON", "WIFI", "MEMBERSHIP"으로 구분
- metadata JSON: 타입별 특수 데이터 저장
- 일관된 UX: 통일된 추가/조회/공유 플로우
- QR 표준 형식:
WIFI:T:WPA;S:MyNetwork;P:MyPassword;H:false;; - Android 10+ 지원: WifiNetworkSuggestion API 활용
- 클릭 한 번 연결: QR 스캔 후 저장된 WiFi 정보로 자동 연결
- 보안: 비밀번호 Android Keystore 암호화 저장
- 그룹 기반 공유: 가족/지인 그룹 생성 및 관리
- 초대 기능: QR코드나 링크로 그룹 초대
- 전체 권한: 모든 멤버가 기프티콘 추가/사용/삭제 가능
- WiFi QR 공유: WiFi 정보를 QR코드로 저장/공유
- 멤버십 공유: 투썸 등 멤버십 번호 공유
- 작성자 표시: 누가 공유했는지 표시
- 오프라인 지원: 오프라인에서도 사용 가능, 온라인 시 동기화
- 실시간 동기화: 실시간으로 변경사항 반영
- gifticon_table ✅ - 기프티콘 메인 정보
- usage_history_table ✅ - 사용 기록
- brand_location_table ❌ - 매장 위치 캐시
- brand_section_table ❌ - 검색 캐시
users/{userId} {
email: string,
displayName: string,
profileImage?: string,
currentGroupId?: string, // 현재 활성 그룹
joinedGroups: [groupId], // 가입한 그룹 목록
createdAt: timestamp,
lastActiveAt: timestamp
}
groups/{groupId} {
name: string,
description?: string,
inviteCode: string, // QR코드용 초대 코드
createdBy: userId,
memberIds: [userId], // 멤버 ID 배열
sharedItemIds: [itemId], // 공유된 아이템 ID 배열 (통합)
settings: {
allowInvite: boolean,
maxMembers: number
},
createdAt: timestamp,
updatedAt: timestamp
}
shared_items/{itemId} {
// 공통 필드
type: 'GIFTICON' | 'WIFI' | 'MEMBERSHIP',
name: string, // 기프티콘명/SSID/멤버십명
brand: string, // 브랜드명
displayBrand: string,
code: string, // 바코드/QR코드
codeType: string, // QR_CODE, BARCODE_128, etc
// 이미지 정보
originImageUrl?: string, // Firebase Storage URL
croppedImageUrl?: string, // Firebase Storage URL
croppedRect?: {
left: number, top: number,
right: number, bottom: number
},
// 공유 정보
addedBy: userId,
addedByName: string,
sharedGroups: [groupId], // 공유된 그룹 목록
// 타입별 메타데이터 (JSON)
metadata: {
// GIFTICON
"expireAt"?: timestamp,
"isCashCard"?: boolean,
"totalCash"?: number,
"remainCash"?: number,
"isUsed"?: boolean,
"memo"?: string,
// WIFI
"password"?: string, // 암호화된 비밀번호
"security"?: "WPA" | "WEP" | "OPEN",
"hidden"?: boolean,
"locationName"?: string, // "우리집", "카페"
"locationAddress"?: string,
// MEMBERSHIP
"membershipNumber"?: string,
"membershipType"?: string, // "VIP", "일반"
"holderName"?: string,
"expiryDate"?: timestamp
},
// 동기화 정보
syncStatus: 'synced' | 'pending' | 'conflict',
localId?: string,
createdAt: timestamp,
updatedAt: timestamp,
updatedBy: userId,
updatedByName: string
}
usage_history/{historyId} {
itemId: string, // shared_items ID
itemName: string,
itemType: string, // GIFTICON, WIFI, MEMBERSHIP
usedBy: userId,
usedByName: string,
addedBy: userId,
addedByName: string,
amount?: number, // 사용 금액 (기프티콘만)
location?: {
x: number, y: number,
address?: string
},
groupId: string,
localId?: string,
usedAt: timestamp
}
users/{userId}/syncQueue/{actionId} {
action: 'create' | 'update' | 'delete',
collection: 'gifticons' | 'usageHistory' | 'wifiCredentials' | 'memberships',
documentId: string,
groupId: string,
data: object, // 변경할 데이터
localId?: string, // 로컬 DB ID
timestamp: timestamp,
retryCount: number,
status: 'pending' | 'processing' | 'failed' | 'completed'
}
- gifticon_table: 기존 구조 그대로 유지 (개인 기프티콘용)
- usage_history_table: 삭제 예정 (통합 사용 기록으로 대체)
@Entity(tableName = "shared_items_table")
data class DBSharedItemEntity(
@PrimaryKey(autoGenerate = true) val id: Long?,
@ColumnInfo(name = "firebase_id") val firebaseId: String?,
@ColumnInfo(name = "type") val type: String, // GIFTICON, WIFI, MEMBERSHIP
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "brand") val brand: String,
@ColumnInfo(name = "display_brand") val displayBrand: String,
@ColumnInfo(name = "code") val code: String, // 바코드/QR코드
@ColumnInfo(name = "code_type") val codeType: String,
@ColumnInfo(name = "origin_image_uri") val originImageUri: Uri?,
@ColumnInfo(name = "cropped_image_uri") val croppedImageUri: Uri?,
@ColumnInfo(name = "cropped_rect") val croppedRect: Rect?,
@ColumnInfo(name = "metadata") val metadata: String, // JSON
@ColumnInfo(name = "added_by") val addedBy: String,
@ColumnInfo(name = "added_by_name") val addedByName: String,
@ColumnInfo(name = "sync_status") val syncStatus: String,
@ColumnInfo(name = "created_at") val createdAt: Date,
@ColumnInfo(name = "updated_at") val updatedAt: Date,
@ColumnInfo(name = "updated_by") val updatedBy: String,
@ColumnInfo(name = "updated_by_name") val updatedByName: String
)@Entity(tableName = "groups_table")
data class DBGroupEntity(
@PrimaryKey val id: String, // Firebase group ID
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "description") val description: String?,
@ColumnInfo(name = "invite_code") val inviteCode: String,
@ColumnInfo(name = "created_by") val createdBy: String,
@ColumnInfo(name = "is_owner") val isOwner: Boolean,
@ColumnInfo(name = "joined_at") val joinedAt: Date,
@ColumnInfo(name = "last_sync_at") val lastSyncAt: Date?
)@Entity(
tableName = "group_shared_items_cross_ref",
primaryKeys = ["group_id", "shared_item_id"]
)
data class GroupSharedItemsCrossRef(
@ColumnInfo(name = "group_id") val groupId: String,
@ColumnInfo(name = "shared_item_id") val sharedItemId: Long,
@ColumnInfo(name = "shared_at") val sharedAt: Date
)@Entity(tableName = "unified_usage_history_table")
data class DBUnifiedUsageHistoryEntity(
@PrimaryKey(autoGenerate = true) val id: Long?,
@ColumnInfo(name = "firebase_id") val firebaseId: String?,
@ColumnInfo(name = "item_id") val itemId: Long, // shared_items_table.id
@ColumnInfo(name = "item_name") val itemName: String,
@ColumnInfo(name = "item_type") val itemType: String,
@ColumnInfo(name = "used_by") val usedBy: String,
@ColumnInfo(name = "used_by_name") val usedByName: String,
@ColumnInfo(name = "added_by") val addedBy: String,
@ColumnInfo(name = "added_by_name") val addedByName: String,
@ColumnInfo(name = "amount") val amount: Int?, // 사용 금액 (기프티콘만)
@ColumnInfo(name = "location_x") val locationX: Dms?,
@ColumnInfo(name = "location_y") val locationY: Dms?,
@ColumnInfo(name = "location_address") val locationAddress: String?,
@ColumnInfo(name = "group_id") val groupId: String,
@ColumnInfo(name = "used_at") val usedAt: Date,
@ColumnInfo(name = "sync_status") val syncStatus: String
)@Entity(tableName = "sync_queue_table")
data class DBSyncQueueEntity(
@PrimaryKey(autoGenerate = true) val id: Long?,
@ColumnInfo(name = "action") val action: String, // CREATE, UPDATE, DELETE
@ColumnInfo(name = "collection_name") val collectionName: String,
@ColumnInfo(name = "document_id") val documentId: String,
@ColumnInfo(name = "group_id") val groupId: String?,
@ColumnInfo(name = "data_json") val dataJson: String,
@ColumnInfo(name = "local_id") val localId: Long?,
@ColumnInfo(name = "timestamp") val timestamp: Date,
@ColumnInfo(name = "retry_count") val retryCount: Int,
@ColumnInfo(name = "status") val status: String // PENDING, PROCESSING, COMPLETED, FAILED
)class SyncWorker : CoroutineWorker() {
override suspend fun doWork(): Result {
// 1. 로컬 변경사항을 Firebase로 업로드
// 2. Firebase 변경사항을 로컬로 다운로드
// 3. 충돌 해결
return Result.success()
}
}FirebaseFirestore.getInstance().apply {
firestoreSettings = FirebaseFirestoreSettings.Builder()
.setPersistenceEnabled(true)
.setCacheSizeBytes(FirebaseFirestoreSettings.CACHE_SIZE_UNLIMITED)
.build()
}// 그룹 데이터 실시간 동기화
firestore.collection("groups")
.document(groupId)
.collection("gifticons")
.addSnapshotListener { snapshot, error ->
// 실시간 변경사항 처리
syncWithLocalDatabase(snapshot)
}WIFI:T:WPA2;S:MyNetwork;P:MyPassword;H:false;;
- T: 보안 타입 (WPA/WPA2/WEP/nopass)
- S: SSID (네트워크 이름)
- P: 비밀번호
- H: Hidden 네트워크 여부 (true/false)
class WifiConnectionManager @Inject constructor(
private val context: Context
) {
fun connectToWifi(wifiData: WifiData): Boolean {
val suggestion = WifiNetworkSuggestion.Builder()
.setSsid(wifiData.ssid)
.setWpa2Passphrase(wifiData.password)
.build()
val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager
val status = wifiManager.addNetworkSuggestions(listOf(suggestion))
return status == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
}
}fun showWifiConnectionDialog(wifiData: WifiData) {
val intent = Intent(Settings.ACTION_WIFI_ADD_NETWORKS)
val config = WifiNetworkSuggestion.Builder()
.setSsid(wifiData.ssid)
.setWpa2Passphrase(wifiData.password)
.build()
intent.putParcelableArrayListExtra(
Settings.EXTRA_WIFI_NETWORK_LIST,
arrayListOf(config)
)
startActivityForResult(intent, WIFI_CONNECTION_REQUEST)
}<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>// Android Keystore를 활용한 비밀번호 암호화
class WifiSecurityManager {
fun encryptPassword(password: String, alias: String): String {
// AES 암호화 구현
}
fun decryptPassword(encryptedPassword: String, alias: String): String {
// AES 복호화 구현
}
}-
Firebase 설정
- Firebase Auth, Firestore, Storage 초기화
- 보안 규칙 설정
-
로컬 DB 확장
- shared_items_table, groups_table 추가
- 통합 Entity 및 DAO 구현
-
그룹 관리 기능
- 그룹 생성/가입/초대 UI
- QR코드 초대 시스템
-
통합 추가 플로우
- QR/바코드 스캔 → 타입 자동 인식
- 타입별 메타데이터 파싱 및 저장
-
WiFi 자동 연결
- WifiNetworkSuggestion API 구현
- Android Keystore 암호화
-
멤버십 관리
- 멤버십 번호/바코드 저장
- 만료일 알림
-
동기화 큐 시스템
- 로컬 변경사항 추적
- WorkManager 백그라운드 동기화
-
충돌 해결
- Last-Write-Wins 전략
- 사용자 선택 가능한 충돌 해결
-
Firestore 리스너
- 그룹 데이터 실시간 동기화
- UI 자동 업데이트
-
푸시 알림
- 새 아이템 공유 알림
- 그룹 초대 알림
- Firestore Security Rules: 그룹 멤버만 해당 그룹 데이터 접근 가능
- WiFi 비밀번호 암호화: Android Keystore로 로컬/Firebase 모두 암호화 저장
- 멤버십 정보 보안: 민감한 개인정보 암호화
- 초대 코드 관리: 일회성 또는 만료 시간 설정
- 데이터 검증: 클라이언트/서버 양쪽에서 데이터 무결성 검증