AGENT SKILL
10-shop-manager
MobilePackage ShopManager를 CompoSingleton으로 구현하고 `SHOP_CATALOG` 기반 카탈로그 생성/refresh/잠금해금(`unlock_msg_id`) + 구매(`CanBuy`, `BuyAsync`) 플로우를 적용할 때 사용한다.
- License 许可
- MIT
- 最近更新
- 2026-04-12
- Stars
- ★ 0
- Forks
- 0
01 前言 Front Matter
- name
- 10-shop-manager
- description
- MobilePackage ShopManager를 CompoSingleton으로 구현하고 `SHOP_CATALOG` 기반 카탈로그 생성/refresh/잠금해금(`unlock_msg_id`) + 구매(`CanBuy`, `BuyAsync`) 플로우를 적용할 때 사용한다.
02 资源文件 4
- 参考文档references/submission.md2.2 KB
- 参考文档references/writeup.md1.1 KB
- 参考文档SOURCE.md323 B
- 参考文档TRUST.auto.yaml2.1 KB
03 SKILL.md 正文
10-shop-manager
Status: ACTIVE AppliesTo: v10
ShopManager는 MobilePackage 상점 구매 진입점이며, 카탈로그별 상품 목록을 관리한다.
catalog initialize/refresh operation의 정본은 03-ssot다.
이 문서는 API/구현 포인트를 다루며, 동작 순서 충돌 시 SSOT가 우선한다.
1. Class
public sealed class ShopManager : CompoSingleton<ShopManager>
- namespace:
Devian - asmdef:
Devian.Samples.MobilePackage MobileApplication에서RequireComponent(typeof(ShopManager))로 보장한다.
2. Public API
public GAME_ERROR_TYPE CanBuy(string shop_item_id)
public Task<GameResult<RewardData[]>> BuyAsync(string shop_item_id, CancellationToken ct = default)
public GameResult Initialize()
public GameResult RefreshProducts(bool requireServerTime = true)
public IReadOnlyList<ShopCatalogBase> GetCatalogs()
public ShopCatalogBase GetCatalog(SHOP_CATALOG_TYPE catalog_type)
public T GetCatalog<T>() where T : ShopCatalogBase
public GameResult<ShopProductBase> GetProduct(string shop_item_id)
public GameResult<ShopRewardProductBase> GetRewardProduct(string shop_item_id)
public GameResult<ShopLimitedProductBase> GetLimitedProduct(string shop_item_id)
public GameResult<ShopProductDaily> GetDailyProduct(string shop_item_id)
public GameResult<ShopProductEvent> GetEventProduct(string shop_item_id)
public GameResult<ShopProductGold> GetGoldProduct(string shop_item_id)
public GameResult<ShopProductChest> GetChestProduct(string shop_item_id)
public GameResult<ShopProductPurchase> GetPurchaseProduct(string shop_item_id)
핵심 플로우:
shop_item_id로ShopProductBase조회- 카탈로그 목록은 하드코딩이 아니라
TB_SHOP_CATALOG.GetAll()row로 생성된다. SHOP_ITEM_DAILY카탈로그는 초기화/refresh 시FREE/ADS 고정 + 비고정 5개 선택snapshot으로 생성된다.CHEST/PURCHASE/GOLD는 각SHOP_*테이블의 모든 상품을 생성한다.EVENT는SHOP_ITEM_EVENT.start_time/end_time서버 UTC 구간 안에 있는 상품만 생성한다.SHOP_ITEM_DAILY선택 생성에서 동일shop_item_id(pk)는 중복 선택하지 않는다.- 구매 제한 자동 refresh 체크(서버 시간 기준): 일반 카탈로그는
autoRefreshUtcMs+auto_refresh_days규칙을 사용한다.auto_refresh_days <= 0이면 자동 refresh를 사용하지 않는다. EVENT는auto_refresh_days가 아니라 가장 가까운 미래start_time/end_time경계 시각을autoRefreshUtcMs로 저장해 refresh를 예약한다.unlock_msg_id가 있는 카탈로그는 초기IsLocked=true이며GameMessageManager구독으로unlock_op_type/unlock_value조건 만족 시 해금된다.- 잠긴 카탈로그의 상품은
CanBuy/BuyAsync에서 차단된다. - 구매 제한(
max_count/remainCount) 체크 ShopProductBase.Price(할인 반영 최종가)로 구매 가능 여부/통화 차감을 처리한다. 원가는ShopProductBase.PriceWithoutDiscount를 사용한다.SHOP_ITEM_DAILY.unit_price는 단가다. DAILY 상품의PriceWithoutDiscount는unit_price * snapshot.amount이며 할인은 그 결과에 적용한다.- 카탈로그 분기와 storage 기반 초기 product 구성은
ShopCatalogFactory+ShopCatalogBase계층이 담당한다. ShopManager는 registry/orchestration/구매 검증/결제를 담당한다. ShopManager.onInitAwake()는 catalog를 초기화하지 않으며,Initialize()가 유일한 manager 초기화 진입점이다.ShopCatalogBase는Initialize() -> onInitialize() -> RefreshProducts() -> onRefresh()라이프사이클을 따른다. product 생성 책임은onRefresh()에 있다.- 기본
onRefresh()는CHEST/PURCHASE/GOLD의 테이블 전체 상품을 생성한다.ShopCatalogDaily.onRefresh()와ShopCatalogEvent.onRefresh()가 고유 생성 로직을 override 한다. PURCHASE는PurchaseManager.PurchaseAsync(internal_product_id)위임, 그 외는 통화 차감/광고 시청 후RewardManager지급- 성공 시
remainCount와 DAILY snapshot 상태를 저장 + SaveData 저장 - ADS/FREE 상품 리필은 카탈로그 저장 버킷의
adsRefreshUtcMs로 별도 관리하며, ADS/FREE 구매 성공 시serverNow + 1day로 기록한다. - 카탈로그 초기화/refresh 시
adsRefreshUtcMs가 없거나 만료면 ADS/FREE 제한 상품을remainCount=max_count로 리필한다. - refresh 조건 탐색은
ShopManager.evaluateCatalogRefreshState(...)한 곳에서 처리한다. - 카탈로그 refresh 실행은
ShopManager.tryRefreshCatalog(...)한 경로에서 처리한다. - SaveData 로드는
ShopStorage에만 반영되고, 런타임 카탈로그 반영은 이후LoginManager가 호출하는ShopManager.Initialize()에서 수행한다. SHOP_ITEM_DAILY의 모든 상태(FREE/ADS포함,amount포함)는dailyCatalogProducts에 저장한다. DAILY 전용productRemainCounts중복 저장은 금지한다.- 카탈로그별 남은 시간 조회는
ShopCatalogBase.RemainAutoRefreshTimeMs를 공통으로 사용하고, DAILY만ShopCatalogDaily.RemainAdsRefreshTimeMs,ManualRefreshRemainTimeMs,ManualRefreshRemainCount를 사용한다. - 로그인 초기화 마지막 단계에서
LoginManager가Initialize()를 호출해 Shop 카탈로그 상태를 최종 정합화한다. GetCatalog<T>()는 typed catalog 획득용이다. 사용 예:ShopManager.Instance.GetCatalog<ShopCatalogDaily>().ShopCatalogBase.ResetAds()와ShopCatalogDaily.RefreshByAdsAsync()는 catalog instance public API다.ShopCatalogDaily.RefreshByAdsAsync()는 daily catalog 내부에서 manual refresh 상태 판단, 광고 시청, 성공 기록을 직접 처리한다.ShopCatalogDaily.RefreshByAdsAsync()는 rolling 24시간 기준 최대 5회만 허용한다. 상태는manualRefreshUtcMs,manualRefreshRemainCount로 관리하며,manualRefreshRemainCount는 남은 횟수다.ShopCatalogDaily.RefreshByAdsAsync()제한 초과 시GAME_ERROR_TYPE.SHOP_ITEM_DAILY_MANUAL_REFRESH_COUNT_EXHAUSTED를 반환한다.ShopManager는syncCatalogRuntimeStates()로 catalog별 runtime state 동기화를 공통 처리한다.ShopManager.ensureCatalogInitialized()는CreateRuntimeCatalogs(storage)로 catalog를 먼저 등록하고, 두 번째 pass에서Initialize()를 호출한다.CreateRuntimeCatalogs(storage)는 catalog 생성 시 typed storage data를 같이 주입한다.ShopManager.synchronizeProductIndexFromCatalogs()는 product index와 limited shop id 인덱스만 재구성한다. remain/storage 복원 책임은 가지지 않는다.
3. Currency Rules
FREE: 가격 0 구매로 사용(차감 없음)ADS: 광고 시청 성공 시 구매 성공JEWEL:JEWEL_FREE우선 차감 후 부족분JEWEL_PAID차감NO_ADS대여(InventoryStorage.GetRentalRemainingMs("NO_ADS") > 0) 중이면CanBuy의AdsManager.CanShow체크와BuyAsync의 광고 show를 skip한다.SHOP_DISCOUNT_TYPE이 설정된 상품은ShopProductBase.Price(할인 반영 값)로 지갑 잔액 검증/차감을 수행한다.BuyAsync의 통화 상품 구매는InventoryManager.Storage.Wallet에서Currency_type기준으로Price만큼 차감한다.BuyAsync차감 경로에서Price < 0은 즉시 차단하고SHOP_PRODUCT_PRICE_INVALID를 반환한다. (음수 가격으로 재화 증감 금지)- 통화 부족 시
GAME_ERROR_TYPE.SHOP_CURRENCY_INSUFFICIENT를 반환한다. Initialize()이전의RefreshProducts/CanBuy/BuyAsync와 catalog public API는GAME_ERROR_TYPE.SAVEDATA_SYNC_REQUIRED로 실패한다.ShopCatalogBase.ResetAds(): 지정 카탈로그를 강제 refresh한다. (forceCatalogRefresh=true)ShopCatalogDaily.RefreshByAdsAsync(): DAILY 수동 refresh를 수행한다. 광고 실패는GAME_ERROR_TYPE.SHOP_ADS_SHOW_FAILED, 횟수 소진은GAME_ERROR_TYPE.SHOP_ITEM_DAILY_MANUAL_REFRESH_COUNT_EXHAUSTED를 반환한다.ShopCatalogDaily.RefreshByAdsAsync()는 global refresh를 호출하지 않고 DAILY만 1회 refresh한다.- SaveData 저장은 ShopManager 초기화 루틴이 아니라 mutation 경로에서 수행한다. 일반 refresh/buy는 ShopManager 저장 경로를 사용하고, DAILY manual refresh는
ShopCatalogDaily.RefreshByAdsAsync()가 로컬 저장을 직접 수행한다.
4. Catalog Rules
DAILY,CHEST,GOLD:RewardManager지급PURCHASE:PurchaseManager구매 처리 사용 (BuyAsync보상 지급 로직 직접 사용 금지)season_id구매 제한(시즌 종료 임박 차단)은 ShopManager에서 검사한다.
5. Error Rules
CanBuy성공:GAME_ERROR_TYPE.SUCCESSCanBuy실패:checkCanBuy()에서 계산된GAME_ERROR_TYPE을 그대로 반환한다.SHOP_CAN_BUY_FAILEDwrapper나LastCanBuy*상태 필드는 사용하지 않는다.BuyAsync실패: 기본적으로GAME_ERROR_TYPE.SHOP_BUY_FAILED- 단, 카탈로그 잠금 실패는
GAME_ERROR_TYPE.GAME_INVALID_ARGUMENT을 반환한다. - 단, 통화 부족 실패는
GAME_ERROR_TYPE.SHOP_CURRENCY_INSUFFICIENT를 그대로 반환한다. - inner 실패 코드는 메시지에
inner=...형태로 포함한다.
6. Implementation Location (3-path mirror)
- UPM (정본):
framework-cs/upm/com.devian.foundation/Samples~/MobilePackage/Runtime/Shop/ShopManager.cs - Packages (sync):
framework-cs/apps/UnityExample/Packages/com.devian.foundation/Samples~/MobilePackage/Runtime/Shop/ShopManager.cs - Assets/Samples (import):
framework-cs/apps/UnityExample/Assets/Samples/Devian Samples/{version}/MobilePackage/Runtime/Shop/ShopManager.cs