최종 업데이트: 2026-08-11 (v5.41) Android / Kotlin + Jetpack Compose + Room + Firebase
Gradle 멀티 모듈 프로젝트입니다 (settings.gradle.kts →
include(":app", ":staff-app")).
| 모듈 | 패키지 | 대상 | 설명 |
|---|---|---|---|
:app |
com.hmhworks.ourshop |
사장님 | 매장·직원·출퇴근·급여·스케줄 전체 관리 |
:staff-app |
com.hmhworks.ourshop.staff |
직원 | 초대코드로 연결 후 본인 스케줄·출퇴근·급여 조회 |
두 앱은 Firestore를 통해서만 연결됩니다. 직원 앱은 Room DB를 쓰지 않고 Firestore를 직접 읽습니다.
ourshop/
├── settings.gradle.kts ← include(":app", ":staff-app")
├── build.gradle.kts ← 루트 빌드 설정
├── gradle/libs.versions.toml ← 버전 카탈로그 (모든 의존성)
├── keystore.properties ← 릴리즈 서명 키 (git 제외)
│
├── app/ ══ 사장님 앱 ══
│ ├── build.gradle.kts ← flavor(dev/prod/prodPro) + 서명 + APK 자동 복사
│ ├── google-services.json ← Firebase 설정
│ └── src/main/java/com/hmhworks/ourshop/
│ ├── MainActivity.kt ← 진입점
│ ├── OurshopApp.kt ← Application (Room·AdMob·알림채널 초기화)
│ │
│ ├── data/
│ │ ├── model/
│ │ │ ├── Staff.kt ← Staff 엔티티, EmploymentType, AvatarType, AttendStatus
│ │ │ ├── AttendanceRecord.kt ← AttendanceRecord 엔티티
│ │ │ ├── PayrollRecord.kt ← PayrollRecord, AdjustmentEntry, Store, Schedule 엔티티
│ │ │ └── BackupData.kt ← 백업/복원·클라우드 동기화 전송 묶음
│ │ │
│ │ ├── local/ ← Room 영속 계층
│ │ │ ├── OurshopDatabase.kt ← @Database(version=5) + MIGRATION_1_2 ~ 4_5
│ │ │ ├── Converters.kt ← List<Int>·List<String>·List<AdjustmentEntry> ↔ JSON
│ │ │ └── dao/
│ │ │ ├── StaffDao.kt
│ │ │ ├── AttendanceDao.kt
│ │ │ ├── PayrollDao.kt
│ │ │ ├── ScheduleDao.kt ← repeatGroupId 삭제 쿼리 포함
│ │ │ └── StoreDao.kt
│ │ │
│ │ └── repository/
│ │ ├── AppRepository.kt ← 단일 진실 공급원 (메모리 상태 + DB write-through)
│ │ └── FirestoreRepository.kt← PRO 클라우드 동기화 + 초대코드 + 직원 링크
│ │
│ ├── ui/
│ │ ├── OurshopNavHost.kt ← Routes + NavHost + HorizontalPager 5탭 + EmojiBottomNav
│ │ │
│ │ ├── components/
│ │ │ ├── StaffAvatar.kt ← 9색 그라디언트 팔레트, staffAvatarGradient()
│ │ │ ├── EmptyState.kt ← 공용 빈 상태
│ │ │ ├── AdBanner.kt ← AdMob 배너 (FREE 전용)
│ │ │ └── InterstitialAdHelper.kt ← 전면 광고
│ │ │
│ │ ├── screens/
│ │ │ ├── SplashScreen.kt ← 스플래시 + 자동 로그인 판정
│ │ │ ├── LoginScreen.kt ← 로그인/회원가입 탭 + Google 로그인
│ │ │ ├── OnboardingScreen.kt ← 신규 유저 매장 이름·아이콘 설정
│ │ │ ├── StoreSelectScreen.kt ← 매장 선택 + AddStoreSheet
│ │ │ ├── HomeScreen.kt ← 홈 + WeeklyReportSheet + HomeStaffDetailSheet + VoiceQuerySheet + MilestoneOverlay
│ │ │ ├── StaffScreen.kt ← 직원 + StaffDetailSheet + ResignConfirmSheet + ResignedStaffDetailSheet + EmploymentContractSheet(SignatureCanvas)
│ │ │ ├── ScheduleScreen.kt ← 스케줄 + CalendarGrid + DayPanel + ScheduleSheet + ShiftBoardTab
│ │ │ ├── AttendanceScreen.kt ← 출퇴근 + AttendDetailSheet + SparklePopup
│ │ │ ├── PayrollScreen.kt ← 급여 + PayrollDetailSheet + AdjustSheet + SettleSheet + PayHistorySheet
│ │ │ ├── AddStaffSheet.kt ← 직원 추가/수정 (색상 피커·사진 촬영)
│ │ │ ├── ContactImportSheet.kt← 연락처에서 직원 일괄 등록
│ │ │ ├── ProfileSheet.kt ← 프로필 + 하위 시트 6종 (아래 흐름도 참조)
│ │ │ ├── ProGateSheet.kt ← PRO 기능 잠금 안내 / 업그레이드 유도
│ │ │ └── PlaceholderScreen.kt ← 개발 중 화면
│ │ │
│ │ ├── viewmodel/
│ │ │ └── MainViewModel.kt ← 앱 전역 상태 (Compose State 위임)
│ │ │
│ │ └── theme/
│ │ ├── AppTheme.kt ← AppColors 토큰 + DEFAULT/WARM 2종 + LocalAppColors
│ │ ├── Color.kt / Type.kt / Theme.kt ← Material3 기본 테마
│ │
│ ├── receiver/
│ │ └── NotifyReceiver.kt ← AlarmManager 브로드캐스트 수신 → 알림 발송
│ │
│ └── utils/
│ ├── PaydayCalculator.kt ← 급여일 D-day 계산
│ ├── StaffFilterUtils.kt ← 직원 필터 + 검색
│ ├── AccountNumberFormatter.kt← 은행별 계좌번호 포맷
│ ├── NotificationHelper.kt ← 채널 생성 + 알림 5종 발송
│ └── NotifyScheduler.kt ← 급여일·미체크·주간리포트 알람 예약
│
├── staff-app/ ══ 직원 앱 ══
│ ├── build.gradle.kts
│ └── src/main/java/com/hmhworks/ourshop/staff/
│ ├── MainActivity.kt
│ ├── StaffNavHost.kt ← 루트 NavHost + 하단 4탭 NavigationBar
│ ├── data/
│ │ ├── model/StaffModels.kt ← WorkplaceInfo, StaffScheduleEntry, StaffAttendRecord, StaffPayEntry, InviteCodeResult
│ │ └── repository/StaffRepository.kt ← Firestore 직접 read/write
│ ├── ui/
│ │ ├── screens/ ← Splash · Login · InviteCode · Home · Schedule · Attendance · Pay
│ │ ├── viewmodel/StaffViewModel.kt
│ │ └── theme/StaffTheme.kt
│
├── docs/
│ ├── ourshop_architecture.md ← 본 파일 (구성도)
│ ├── ourshop_guide.md ← 기능 가이드
│ ├── ourshop_user_guide.html ← 사용자 가이드 (HTML)
│ ├── ourshop_patch_notes.md ← 패치 이력
│ ├── ourshop_staff_app_design.md ← 직원 앱 설계
│ ├── monetization_guide.md / ourshop_monetization.md ← 수익화
│ ├── schedule_mockup_v*.html ← 스케줄 UI 시안
│ └── patch_notes_backup/ ← 일별 패치 노트 백업
│
├── scripts/
│ ├── build.ps1 ← Gradle 빌드만
│ ├── install.ps1 ← ADB 연결 + APK 설치
│ └── build-install.ps1 ← 빌드 + 설치 통합
│
├── gitmessage ← 커밋 메시지 초안
└── CLAUDE.md ← Claude Code 프로젝트 지침
flavorDimensions = "env")| Flavor | applicationId | IS_DEV | MOCK_PRO | 광고 ID | 용도 |
|---|---|---|---|---|---|
dev |
...ourshop.dev |
true |
true |
테스트 ID | Mock 데이터, PRO 전체 개방, DB 미사용 |
prod |
...ourshop |
false |
false |
실제 ID | 실제 배포. Room + Firebase 사용 |
prodPro |
...ourshop.pro |
false |
true |
실제 ID | PRO 검증용. 광고 없음, prod와 동시 설치 |
dev는 applicationIdSuffix가 달라
prod 앱과 기기에 동시 설치 가능합니다.BuildConfig.IS_DEV / MOCK_PRO /
IS_RELEASE / ADMOB_*_ID 필드가
flavor·buildType별로 주입됩니다.| 항목 | 값 |
|---|---|
| compileSdk / targetSdk | 35 |
| minSdk | 26 |
| Java / JVM target | 17 |
| release | minify + shrinkResources + keystore
서명 |
| APK 이름 | ourshop-{flavor}-{buildType}-v{versionName}.apk |
| APK 자동 복사 | 빌드 후 ../app 폴더로 복사 |
Compose BOM · Material3 · Navigation Compose · Room(KSP) · DataStore · Gson · Firebase(Auth · Firestore · Analytics) · Credential Manager(Google 로그인) · Coil · AdMob
SplashScreen
└─ prod이고 FirebaseAuth 세션이 남아 있으면 → 로그인 건너뜀
├─ 세션 있음 → StoreSelectScreen
└─ 세션 없음 → LoginScreen
LoginScreen (로그인 / 회원가입 탭 + Google 로그인)
└─ 성공 → StoreSelectScreen
StoreSelectScreen
├─ DB 로드 대기 중 → CircularProgressIndicator
├─ 신규 유저(기본 매장만 + 직원 0명) → OnboardingScreen
│ └─ 매장 이름·아이콘 입력 → MAIN
└─ 매장 선택 → MAIN
└─ AddStoreSheet (매장 추가)
MAIN — HorizontalPager 5탭 + EmojiBottomNav (스와이프 전환)
│ FREE 플랜이면 하단에 AdBanner 노출
│
├─ 🏡 홈 (HomeScreen)
│ ├─ 프로필 버튼 → ProfileSheet
│ │ ├─ PaydaySettingSheet ← 급여일 설정
│ │ ├─ ContractTemplateSheet ← 근로계약서 기본 조항
│ │ ├─ NotificationSheet ← 알림 설정
│ │ ├─ BackupRestoreSheet ← 백업 / 복원
│ │ ├─ AddStoreSheet ← 매장 추가
│ │ ├─ ThemeOptionCard ← DEFAULT / WARM 테마 전환
│ │ └─ AppInfoSheet ← 앱 정보
│ ├─ 직원 행 탭 → HomeStaffDetailSheet (출근/퇴근 처리)
│ ├─ 빠른 실행 타일 → 직원 추가 / 일정 추가 / 급여 탭 이동
│ ├─ 주간 리포트 → WeeklyReportSheet
│ ├─ 음성 질의 → VoiceQuerySheet
│ └─ 직원 수 달성 시 → MilestoneOverlay (1·5·10·15·20명, 이후 5명 단위)
│
├─ 🪪 직원 (StaffScreen)
│ ├─ 직원 카드 → StaffDetailSheet
│ │ ├─ 수정 → AddStaffSheet (수정 모드)
│ │ ├─ 근로계약서 → EmploymentContractSheet → SignatureCanvas (사장·직원 서명)
│ │ ├─ 직원 앱 초대코드 발급 (6자리, 24시간 유효)
│ │ └─ 퇴사 처리 → ResignConfirmSheet
│ ├─ 퇴직자 카드 → ResignedStaffDetailSheet (재입사 / 영구 삭제)
│ ├─ 직원 추가 → AddStaffSheet (색상 피커 · 사진 촬영)
│ └─ 연락처 가져오기 → ContactImportSheet (일괄 등록)
│
├─ 🗓️ 스케줄 (ScheduleScreen)
│ ├─ 뷰 전환: 📋 일정(CalendarGrid + DayPanel) ↔ 👥 근무보드(ShiftBoardTab)
│ ├─ 날짜 탭 → DayPanel에 해당 일정 표시
│ ├─ 일정 추가/수정 → ScheduleSheet (+ TimePickerDialog)
│ │ └─ 반복: 없음 / 매주 / 격주 / 매일 / 주말 → repeatGroupId로 묶임
│ ├─ 반복 일정 삭제 → 3-option (이 일정만 / 이후 전체 / 그룹 전체)
│ └─ 자동 생성: 주간 생성 · 월간 생성 · 지난주 복사 (직원 고정 근무요일 기반)
│
├─ 🕐 출퇴근 (AttendanceScreen)
│ ├─ 출근/퇴근 태그 탭 → 처리 + SparklePopup 애니메이션
│ └─ 직원 카드 → AttendDetailSheet (시간 수정 · 상태 변경)
│
└─ 🧾 급여 (PayrollScreen)
├─ 직원 카드 → PayrollDetailSheet
├─ 조정 → AdjustSheet (수당 / 공제 추가·삭제)
├─ 지급 완료 → SettleSheet
└─ 히스토리 → PayHistorySheet
※ PRO 전용 기능 진입 시 FREE 플랜이면 → ProGateSheet
SplashScreen
├─ 로그인 안 됨 → LoginScreen
├─ 로그인됨 + 연결 없음 → InviteCodeScreen
└─ 로그인됨 + 연결 있음 → MAIN
LoginScreen → InviteCodeScreen (사장님이 발급한 6자리 코드 입력)
└─ 성공 → /staffLinks/{staffUid}/workplaces/{storeId} 생성 → MAIN
MAIN — 하단 4탭 NavigationBar (Material3 아이콘, 인디고 #4F46E5)
├─ 🏠 홈 HomeScreen ← 오늘 근무·최근 상태
├─ 📅 스케줄 ScheduleScreen ← 본인 스케줄 조회
├─ ⏰ 출퇴근 AttendanceScreen ← 출근·퇴근 기록
└─ 💳 급여 PayScreen ← 월별 급여 조회
디자인 규칙: 직원 앱에는 👤 사람 아이콘·실루엣을 사용하지 않습니다. 가게(건물) 아이콘을 사용합니다.
┌────────────────────────────────────────────────────────────────┐
│ UI Layer (Compose) │
│ Screens · BottomSheets · Components(StaffAvatar, AdBanner…) │
│ 테마 토큰은 LocalAppColors(CompositionLocal)로 주입 │
└───────────────────────────┬────────────────────────────────────┘
│ Compose State 읽기 / 메서드 호출
┌───────────────────────────▼────────────────────────────────────┐
│ ViewModel Layer │
│ MainViewModel │
│ - AppRepository 상태를 그대로 위임 (staffList, schedules …) │
│ - derivedStateOf 파생값: activeStaff / filteredStaff / │
│ inCount · pendingCount · outCount · offCount / paydayDday │
│ - isPro · isDbLoaded · isNewUser · cloudSyncState · appTheme │
└───────────────────────────┬────────────────────────────────────┘
│
┌───────────────────────────▼────────────────────────────────────┐
│ Repository Layer │
│ AppRepository (object — 단일 진실 공급원) │
│ · mutableStateListOf 로 메모리 상태 보유 │
│ · 모든 변경은 write-through: 메모리 즉시 반영 → DB 비동기 저장 │
│ · PRO + 로그인 상태면 Firestore에도 fire-and-forget 업서트 │
└──────────┬──────────────────────────────┬──────────────────────┘
│ │
┌──────────▼───────────────┐ ┌───────────▼──────────────────────┐
│ Room (로컬 · 필수) │ │ Firestore (클라우드 · PRO 전용) │
│ ourshop.db version 5 │ │ FirestoreRepository │
│ staff / attendance_ │ │ /users/{uid}/{collection}/{id} │
│ records / payroll_ │ │ /inviteCodes/{code} │
│ records / schedules / │ │ /staffLinks/{staffUid}/ │
│ stores │ │ workplaces/{storeId} │
└───────────────────────────┘ └──────────────────────────────────┘
MainViewModel → AppRepository를 거칩니다.AppRepository의 메모리 상태가 화면의 기준입니다. DB
저장은 뒤따라 일어납니다(write-through).dev flavor는 DB를 거치지 않고 Mock 데이터를 메모리에
바로 올립니다 (IS_DEV 분기).DB 이름 ourshop.db · version 5 ·
exportSchema = false ·
@TypeConverters(Converters)
@Entity(tableName = "staff")
data class Staff(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val name: String,
val position: String = "",
val employmentType: EmploymentType = EmploymentType.PARTTIME,
val hourlyWage: Int = 9860,
val phone: String = "",
val bankName: String = "",
val accountNumber: String = "",
val avatarType: AvatarType = AvatarType.INITIAL, // INITIAL / EMOJI / PHOTO
val avatarValue: String = "", // 이모지 또는 사진 URI
val avatarColorIndex: Int = -1, // -1=자동, 0~8=고정
val attendStatus: AttendStatus = AttendStatus.PENDING,
val hireDate: String = "",
val isShortTerm: Boolean = false,
val endDate: String = "",
val isRetired: Boolean = false,
val retireDate: String = "",
val retireReason: String = "",
val contractSaved: Boolean = false,
val ownerSignature: String = "",
val staffSignature: String = "",
val startDate: String = "",
val paymentMethod: String = "계좌이체",
val accountHolder: String = "",
val memo: String = "",
val fixedWorkDays: List<Int> = emptyList(), // 1=월 … 7=일 (DayOfWeek.value)
val fixedStartTime: String = "", // "HH:mm"
val fixedEndTime: String = "", // "HH:mm"
val storeId: String = "store1",
val staffUid: String = "", // 직원 앱 연결 시 채워짐
val staffEmail: String = "",
)
enum class EmploymentType(val label: String) {
FULLTIME("정규직"), PARTTIME("파트타임"),
PARTIME_WORKER("아르바이트"), SHORTTERM("단기")
}
enum class AvatarType { INITIAL, EMOJI, PHOTO }
enum class AttendStatus(val label: String, val colorHex: String) {
IN("출근중", "#22C55E"), PENDING("출근전", "#F59E0B"),
OUT("퇴근", "#3B82F6"), OFF("휴무", "#9CA3AF")
}@Entity(tableName = "attendance_records")
data class AttendanceRecord(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val staffId: String,
val date: String, // YYYY-MM-DD
val checkIn: String = "", // HH:mm
val checkOut: String = "", // HH:mm
val status: AttendStatus = AttendStatus.PENDING,
val memo: String = "",
val storeId: String = "store1",
)@Entity(tableName = "payroll_records")
data class PayrollRecord(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val staffId: String,
val year: Int,
val month: Int,
val totalHours: Double = 0.0,
val workDays: Int = 0,
val baseAmount: Int = 0,
val adjustmentLog: List<AdjustmentEntry> = emptyList(), // TypeConverter → JSON
val isPaid: Boolean = false,
val paidDate: String = "",
val storeId: String = "store1",
) {
@get:Ignore val bonusAmount: Int get() = adjustmentLog.filter { it.type == "bonus" }.sumOf { it.amount }
@get:Ignore val deductAmount: Int get() = adjustmentLog.filter { it.type == "deduct" }.sumOf { it.amount }
@get:Ignore val totalAmount: Int get() = baseAmount + bonusAmount - deductAmount
}
data class AdjustmentEntry( // 엔티티 아님 — JSON으로 직렬화되어 저장
val id: String = UUID.randomUUID().toString(),
val type: String, // "bonus" | "deduct"
val amount: Int,
val memo: String,
val date: String, // yyyy-MM-dd HH:mm
)@Entity(tableName = "schedules")
data class Schedule(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val date: String, // YYYY-MM-DD
val title: String,
val startTime: String = "", // HH:mm
val endTime: String = "", // HH:mm
val staffIds: List<String> = emptyList(), // TypeConverter → JSON
val color: String = "#FF6B35",
val storeId: String = "store1",
val repeatGroupId: String? = null, // 반복 일정 묶음 ID
)@Entity(tableName = "stores")
data class Store(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val name: String,
val icon: String = "🏪",
val paydayDate: Int = 25,
)data class BackupData(
val version: Int = 1,
val exportedAt: String = "",
val stores: List<Store>, val staff: List<Staff>,
val attendance: List<AttendanceRecord>, val payroll: List<PayrollRecord>,
val schedules: List<Schedule>,
val userName: String, val userEmail: String, val currentStoreId: String,
)| 버전 | 마이그레이션 | 변경 내용 |
|---|---|---|
| 1 → 2 | MIGRATION_1_2 |
staff에 fixedWorkDays ·
fixedStartTime · fixedEndTime 추가 |
| 2 → 3 | MIGRATION_2_3 |
4개 테이블 전체에 storeId 추가 (멀티 매장 지원) |
| 3 → 4 | MIGRATION_3_4 |
staff에 staffUid · staffEmail
추가 (직원 앱 연결) |
| 4 → 5 | MIGRATION_4_5 |
schedules에 repeatGroupId 추가 (반복
일정) |
스키마를 바꿀 때는 반드시
version을 올리고 새Migration객체를addMigrations(...)에 등록해야 합니다. 누락하면 실행 중 크래시가 납니다.
5개 DAO 모두 동일한 패턴을 따릅니다 (suspend 함수,
OnConflictStrategy.REPLACE).
| 공통 함수 | 설명 |
|---|---|
getAll() |
전체 조회 |
getByStore(storeId) |
매장별 조회 (StoreDao 제외) |
insert() / insertAll() |
단건 / 일괄 삽입 (REPLACE) |
update() |
갱신 (ScheduleDao 제외) |
deleteById(id) / deleteByStore(storeId) /
deleteAll() |
삭제 |
ScheduleDao 전용 추가 쿼리
@Query("DELETE FROM schedules WHERE repeatGroupId = :groupId")
suspend fun deleteByRepeatGroupId(groupId: String)
@Query("DELETE FROM schedules WHERE repeatGroupId = :groupId AND date >= :fromDate")
suspend fun deleteByRepeatGroupIdFrom(groupId: String, fromDate: String)/users/{ownerUid}/
stores/{storeId}
staff/{staffId} ← staffUid·staffEmail 필드로 직원 앱 연결 상태 표현
attendance/{recordId}
payroll/{payrollId}
schedules/{scheduleId}
/inviteCodes/{6자리코드}
{ ownerUid, staffId, storeId, expiresAt(24h), used }
/staffLinks/{staffUid}/workplaces/{storeId}
{ isActive, ... } ← 직원 앱이 자기 근무지를 찾는 진입점
Room 엔티티를 Firestore에 그대로 넣으면 어노테이션과 충돌하므로,
Gson을 경유해 Map<String, Any?>으로
변환한 뒤 읽고 씁니다.
private fun Any.toFsMap(): Map<String, Any?> =
gson.fromJson(gson.toJson(this), mapType)watchStaffConnections()가
/users/{ownerUid}/staff에 스냅샷 리스너를 붙입니다. 최초
스냅샷은 기준선으로만 쓰고, 이후 MODIFIED 이벤트에서
staffUid가 새로 채워진 경우에만 콜백이 울립니다 →
사장님 화면에 "○○ 직원이 연결되었습니다 ✅" 표시.
AppTheme.kt가 AppColors 토큰 묶음을
정의하고, LocalAppColors(CompositionLocal)로 전체 화면에
주입합니다. OurshopNavHost가 vm.appTheme 값에
따라 팔레트를 갈아 끼웁니다.
| 토큰 그룹 | 주요 필드 |
|---|---|
| 배경 | background · backgroundSecondary ·
surface |
| 텍스트 | ink · inkSecondary ·
inkTertiary · inkHint |
| 브랜드 | brand · brandDeep ·
brandSoft |
| 헤더 | headerBrush · headerHasBorder ·
headerText · headerIconBg … |
| 히어로 | heroBrush · heroInHeader |
| 버튼/칩/네비 | actionBrush · chipActiveBg ·
navIndicatorBg · navActiveColor |
| DEFAULT | WARM | |
|---|---|---|
| 배경 | #FAFAFA |
#F4EFE9 (웜 뉴트럴) |
| 헤더 | 오렌지 그라디언트, 흰 글씨 | 흰 배경 + 아래 테두리, 검은 글씨 |
| 히어로 카드 | 헤더 안, 오렌지 | 헤더와 분리, 다크 |
| 액션 버튼 | 오렌지 그라디언트 | 다크 그라디언트 |
사용 시: val tc = LocalAppColors.current →
tc.brand, tc.isWarm 등으로 분기.
StaffAvatar.kt)직원별 색상은 9색 팔레트를 인덱스로 순환해서 씁니다. 아바타 배경과 직원명 텍스트가 같은 함수를 공유합니다.
private val avatarGradients = listOf(
listOf(Color(0xFFFF6B35), Color(0xFFFF9A6C)), // 0 orange
listOf(Color(0xFF2EC4B6), Color(0xFF5EEAD4)), // 1 teal
listOf(Color(0xFF8B5CF6), Color(0xFFA78BFA)), // 2 purple
listOf(Color(0xFFEC4899), Color(0xFFF472B6)), // 3 pink
listOf(Color(0xFF3B82F6), Color(0xFF60A5FA)), // 4 blue
listOf(Color(0xFF10B981), Color(0xFF34D399)), // 5 green
listOf(Color(0xFFF59E0B), Color(0xFFFBBF24)), // 6 amber
listOf(Color(0xFFEF4444), Color(0xFFF87171)), // 7 red
listOf(Color(0xFF6366F1), Color(0xFF818CF8)), // 8 indigo
)
fun staffAvatarGradient(index: Int): List<Color> =
avatarGradients[index.coerceAtLeast(0) % avatarGradients.size]
fun staffAvatarColor(index: Int): Color = staffAvatarGradient(index).first()StaffAvatar는
staff.avatarColorIndex >= 0이면 그 값을, 아니면 목록
순서 index를 씁니다.avatarType에 따라 PHOTO(Coil
AsyncImage) / EMOJI /
INITIAL(이름 첫 글자)로 렌더링합니다.// 활성 직원 — brush가 우선하므로 color 파라미터는 쓰지 않음
Text(text = staff.name,
style = TextStyle(brush = Brush.linearGradient(staffAvatarGradient(index))))
// 퇴직자 — 회색 고정
Text(text = staff.name, style = TextStyle.Default, color = Color(0xFF9CA3AF))ProfileSheet(알림 설정)
└─ NotifyScheduler ── AlarmManager 예약 ──▶ NotifyReceiver ──▶ NotificationHelper ──▶ 알림 발송
예약 함수 (NotifyScheduler) |
발송 함수 (NotificationHelper) |
내용 |
|---|---|---|
schedulePaydayAlarms() |
sendPayday() |
급여일 D-day 알림 |
scheduleDailyUnchecked() |
sendUnchecked() |
미체크 직원 일일 알림 |
scheduleWeeklyReport() |
sendWeeklyReport() |
주간 리포트 |
| (즉시) | sendCheckIn() / sendCheckOut() |
출근·퇴근 처리 알림 |
알림 채널은 OurshopApp에서
NotificationHelper.createChannels()로 생성하며, 설정값은
SharedPreferences(notifyPrefs)에 저장됩니다.
| 구분 | FREE | PRO |
|---|---|---|
| 배너 광고 | AdBanner 노출 (OurshopNavHost 하단) |
없음 |
| 전면 광고 | InterstitialAdHelper |
없음 |
| 클라우드 동기화 | ✕ | Firestore 자동 동기화 |
| PRO 기능 진입 | ProGateSheet 안내 |
바로 실행 |
MainViewModel.isPro →
AppRepository.isPro. 초기값은
BuildConfig.MOCK_PRO.dev와 prodPro는
MOCK_PRO = true라 PRO 기능이 전부 열립니다.BuildConfig.ADMOB_*_ID로 주입되어,
dev에서는 항상 Google 테스트 ID를 씁니다 (정책 위반
방지).| 경로 | 흐름 |
|---|---|
| 로컬 백업 | exportBackupJson() →
AppRepository.createBackup() → Gson → JSON 파일 |
| 로컬 복원 | JSON → Gson → restoreFromBackup(BackupData) → DB 전체
교체 + 메모리 갱신 |
| 클라우드 업로드 | FirestoreRepository.uploadAll(uid, backup) |
| 클라우드 다운로드 | syncFromCloud() → downloadAll(uid) →
restoreFromBackup() |
동기화 상태는
MainViewModel.cloudSyncState(Idle /
Syncing / Done / Error)로 UI에
노출됩니다.
# 위치: scripts/ — Windows PowerShell 전용
.\scripts\build.ps1 # 빌드만 (APK 생성)
.\scripts\install.ps1 -Target <ip:port> # ADB 연결 + 설치
.\scripts\build-install.ps1 -Target <ip:port> # 빌드 + 설치 (가장 많이 사용)주의: ADB 포트는 무선 디버깅 세션마다 바뀝니다. 매번 기기에서 확인 후 파라미터로 넘기세요.
docs/ 문서는 Oracle Cloud 서버에 정적 페이지로 배포되어
있습니다.
/var/www/html/ourshop/ (nginx·apache 설정
변경 없이 폴더만 추가)index.html(목록) · guide.html ·
manual.html · arch.html ·
patch.html · mockup.html ·
style.csspandoc으로 수행합니다.
pandoc x.md --from gfm --to html5 --standalone --toc --toc-depth=2 --css style.css