← 문서 목록

우리매장 전체 구성도

우리매장 앱 — 전체 구성도

최종 업데이트: 2026-08-11 (v5.41) Android / Kotlin + Jetpack Compose + Room + Firebase


모듈 구성

Gradle 멀티 모듈 프로젝트입니다 (settings.gradle.ktsinclude(":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 프로젝트 지침

빌드 구성

Product Flavors (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와 동시 설치

기타 설정

항목
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}        │
└───────────────────────────┘  └──────────────────────────────────┘

데이터 흐름 규칙

  1. UI는 Room·Firestore를 직접 만지지 않습니다. 항상 MainViewModelAppRepository를 거칩니다.
  2. AppRepository의 메모리 상태가 화면의 기준입니다. DB 저장은 뒤따라 일어납니다(write-through).
  3. dev flavor는 DB를 거치지 않고 Mock 데이터를 메모리에 바로 올립니다 (IS_DEV 분기).
  4. Firestore 쓰기는 실패해도 앱 동작을 막지 않습니다 (fire-and-forget).

데이터 모델 (Room 엔티티)

DB 이름 ourshop.db · version 5 · exportSchema = false · @TypeConverters(Converters)

staff

@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")
}

attendance_records

@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",
)

payroll_records

@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
)

schedules

@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
)

stores

@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,
)

DB 마이그레이션 이력

버전 마이그레이션 변경 내용
1 → 2 MIGRATION_1_2 stafffixedWorkDays · fixedStartTime · fixedEndTime 추가
2 → 3 MIGRATION_2_3 4개 테이블 전체에 storeId 추가 (멀티 매장 지원)
3 → 4 MIGRATION_3_4 staffstaffUid · staffEmail 추가 (직원 앱 연결)
4 → 5 MIGRATION_4_5 schedulesrepeatGroupId 추가 (반복 일정)

스키마를 바꿀 때는 반드시 version을 올리고 새 Migration 객체를 addMigrations(...)에 등록해야 합니다. 누락하면 실행 중 크래시가 납니다.


DAO 구조

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)

Firestore 구조 (PRO 전용)

/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.ktAppColors 토큰 묶음을 정의하고, LocalAppColors(CompositionLocal)로 전체 화면에 주입합니다. OurshopNavHostvm.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.currenttc.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()

직원명 그라디언트 텍스트 패턴

// 활성 직원 — 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)에 저장됩니다.


광고 · PRO 게이팅

구분 FREE PRO
배너 광고 AdBanner 노출 (OurshopNavHost 하단) 없음
전면 광고 InterstitialAdHelper 없음
클라우드 동기화 Firestore 자동 동기화
PRO 기능 진입 ProGateSheet 안내 바로 실행

백업 · 복원 · 클라우드 동기화

경로 흐름
로컬 백업 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 서버에 정적 페이지로 배포되어 있습니다.