reward.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. // D3 재화/보상/레벨/응원/지위아이템 도메인 타입
  2. // Backend Application/Features/Api/{Rewards/GetToday, Member/GetLevel, Member/Items/*, Forum/PostCheer/*}/Response.cs 대응.
  3. // ⚠️ Web.Api Program.cs 에 JsonStringEnumConverter 미등록 → enum 은 전부 숫자로 직렬화됨
  4. // (ItemKind·EquipSlot 은 정수 코드로 옴 — types/community.ts 와 동일 convention).
  5. // 라벨 맵은 이 파일 하단에 한 곳으로 모은다 (지시 6번 — ItemKind/EquipSlot 라벨 단일 소스).
  6. // ── 재화 (WalletBalanceType) — UI 이원화: 캐시 / 토큰 (d0 §2.2, Frontend/CLAUDE.md 용어 규칙) ──
  7. // 캐시 = PgCharged + Deposit + Adjusted (충전 재화, 상점 구매 전용)
  8. // 토큰 = Reward + Airdrop (활동 적립 재화, 응원·모의투자 전용)
  9. export const CASH_BALANCE_TYPES = ['PgCharged', 'Deposit', 'Adjusted'] as const;
  10. export const TOKEN_BALANCE_TYPES = ['Reward', 'Airdrop'] as const;
  11. // GET /api/wallet → { balances: [{ type, amount }] }
  12. export interface WalletBalanceRow {
  13. type: string;
  14. amount: number;
  15. }
  16. export interface WalletResponse {
  17. id: number;
  18. memberID: number;
  19. balance: number;
  20. balances: WalletBalanceRow[];
  21. }
  22. // 캐시/토큰 합산 헬퍼 — balances 배열에서 파티션별 합계
  23. export function sumCash(balances: WalletBalanceRow[]|undefined|null): number {
  24. if (!balances) {
  25. return 0;
  26. }
  27. return balances
  28. .filter(c => (CASH_BALANCE_TYPES as readonly string[]).includes(c.type))
  29. .reduce((acc, c) => acc + c.amount, 0);
  30. }
  31. export function sumToken(balances: WalletBalanceRow[]|undefined|null): number {
  32. if (!balances) {
  33. return 0;
  34. }
  35. return balances
  36. .filter(c => (TOKEN_BALANCE_TYPES as readonly string[]).includes(c.type))
  37. .reduce((acc, c) => acc + c.amount, 0);
  38. }
  39. // 응원 가용액 = 캐시 + 토큰 (Backend Wallet.GetCheerAvailable 대응 — 캐시→토큰 순 차감)
  40. export function sumCheerAvailable(balances: WalletBalanceRow[]|undefined|null): number {
  41. return sumCash(balances) + sumToken(balances);
  42. }
  43. // ── GET /api/members/me/level ──
  44. export interface MemberLevelResponse {
  45. exp: number;
  46. currentGradeID: number|null;
  47. currentGradeName: string|null;
  48. currentGradeImage: string|null;
  49. currentGradeTextColor: string|null;
  50. nextGradeID: number|null;
  51. nextGradeName: string|null;
  52. nextGradeRequiredExp: number|null;
  53. expToNextGrade: number|null;
  54. }
  55. // ── GET /api/rewards/me/today ──
  56. // ActionType 은 tinyint 숫자 (Post=1, Comment=2, LikeGiven=3, LikeReceived=4, Attendance=5, CheerReceived=6, Chat=7, PaperReward=8)
  57. export interface RewardActionSummary {
  58. actionType: number;
  59. actionName: string;
  60. count: number;
  61. expEarned: number;
  62. pointEarned: number;
  63. remainingCount: number|null;
  64. remainingExp: number|null;
  65. remainingPoint: number|null;
  66. }
  67. export interface RewardsTodayResponse {
  68. rewardDate: string;
  69. totalExp: number;
  70. totalPoint: number;
  71. actions: RewardActionSummary[];
  72. }
  73. // ── GET /api/posts/{postID}/cheers ──
  74. export interface CheerItem {
  75. id: number;
  76. fromMemberID: number;
  77. fromNickname: string|null;
  78. amount: number;
  79. message: string|null;
  80. createdAt: string;
  81. }
  82. export interface CheerListResponse {
  83. totalAmount: number;
  84. count: number;
  85. recent: CheerItem[];
  86. }
  87. // POST /api/posts/{postID}/cheers → { cheerID }
  88. export interface CheerSendRequest {
  89. amount: number;
  90. message?: string|null;
  91. }
  92. export interface CheerSendResponse {
  93. cheerID: number;
  94. }
  95. // ── ItemKind (Domain Store/ValueObject/ItemKind.cs — 숫자 직렬화) ──
  96. export const ItemKind = {
  97. Coupon: 1,
  98. Badge: 2,
  99. NicknameEffect: 3,
  100. CommentEffect: 4,
  101. ChatBanLift: 5,
  102. PostReset: 6,
  103. NotePass: 7,
  104. ExpBooster: 8,
  105. ProfileImageChange: 9,
  106. TaglineChange: 10,
  107. NicknameChange: 11
  108. } as const;
  109. export type ItemKindValue = (typeof ItemKind)[keyof typeof ItemKind];
  110. // ── EquipSlot (Domain Members/ValueObject/EquipSlot.cs — 숫자 직렬화) ──
  111. export const EquipSlot = {
  112. Badge: 1,
  113. NicknameEffect: 2,
  114. CommentEffect: 3
  115. } as const;
  116. export type EquipSlotValue = (typeof EquipSlot)[keyof typeof EquipSlot];
  117. // 장착형(지위) 아이템 종류 — 슬롯을 가지는 것들
  118. export const STATUS_ITEM_KINDS: ItemKindValue[] = [ItemKind.Badge, ItemKind.NicknameEffect, ItemKind.CommentEffect];
  119. // ── GET /api/members/me/items (List/Response.Row) ──
  120. // Kind·EquipSlot 은 숫자. isStatusItem = EquipSlot != null.
  121. export interface MemberItemRow {
  122. id: number;
  123. productID: number;
  124. productName: string;
  125. thumbnail: string|null;
  126. kind: ItemKindValue;
  127. equipSlot: EquipSlotValue|null;
  128. isEquipped: boolean;
  129. isStatusItem: boolean;
  130. effectPayload: string|null;
  131. durationDays: number;
  132. acquiredAt: string;
  133. expiresAt: string|null;
  134. usedAt: string|null;
  135. giftFromMemberID: number|null;
  136. giftFromName: string|null;
  137. }
  138. export interface MemberItemsResponse {
  139. total: number;
  140. list: MemberItemRow[];
  141. }
  142. // POST /api/members/me/items/{id}/equip · /unequip → { id, slot, isEquipped }
  143. export interface EquipResponse {
  144. id: number;
  145. slot: EquipSlotValue;
  146. isEquipped: boolean;
  147. }
  148. // ── GET /api/members/equipped-items?ids= (Items/Equipped/Response) ──
  149. export interface EquippedItem {
  150. slot: EquipSlotValue;
  151. kind: ItemKindValue;
  152. productName: string;
  153. effectPayload: string|null;
  154. thumbnail: string|null;
  155. }
  156. export interface MemberEquip {
  157. memberID: number;
  158. items: EquippedItem[];
  159. }
  160. export interface EquippedItemsResponse {
  161. members: MemberEquip[];
  162. }
  163. // EffectPayload JSON — 색상·아이콘·애니메이션 키 (Product.EffectPayload 스냅샷)
  164. // 자유 형식이나 렌더에서 안전하게 참조할 필드만 선언 (없으면 무시).
  165. export interface EffectPayload {
  166. color?: string;
  167. background?: string;
  168. textColor?: string;
  169. icon?: string;
  170. animation?: string;
  171. label?: string;
  172. }
  173. export function parseEffectPayload(raw: string|null|undefined): EffectPayload|null {
  174. if (!raw) {
  175. return null;
  176. }
  177. try {
  178. const parsed = JSON.parse(raw);
  179. if (parsed && typeof parsed === 'object') {
  180. return parsed as EffectPayload;
  181. }
  182. } catch {
  183. // 잘못된 JSON 은 무시 (배지 미표시)
  184. }
  185. return null;
  186. }
  187. // ── 라벨 맵 (지시 6번 — ItemKind / EquipSlot 라벨 단일 소스) ──
  188. export const ITEM_KIND_LABEL: Record<ItemKindValue, string> = {
  189. [ItemKind.Coupon]: '쿠폰',
  190. [ItemKind.Badge]: '배지',
  191. [ItemKind.NicknameEffect]: '닉네임 이펙트',
  192. [ItemKind.CommentEffect]: '댓글 이펙트',
  193. [ItemKind.ChatBanLift]: '채팅 밴 해제권',
  194. [ItemKind.PostReset]: '예전 글 초기화권',
  195. [ItemKind.NotePass]: '쪽지권',
  196. [ItemKind.ExpBooster]: '경험치 2배 모래시계',
  197. [ItemKind.ProfileImageChange]: '프로필 이미지 변경권',
  198. [ItemKind.TaglineChange]: '오늘의 한마디 변경권',
  199. [ItemKind.NicknameChange]: '닉네임 변경권'
  200. };
  201. export const EQUIP_SLOT_LABEL: Record<EquipSlotValue, string> = {
  202. [EquipSlot.Badge]: '배지',
  203. [EquipSlot.NicknameEffect]: '닉네임 이펙트',
  204. [EquipSlot.CommentEffect]: '댓글 이펙트'
  205. };
  206. export const EQUIP_SLOT_DESC: Record<EquipSlotValue, string> = {
  207. [EquipSlot.Badge]: '닉네임 옆에 표시되는 배지입니다.',
  208. [EquipSlot.NicknameEffect]: '닉네임에 색상·강조 효과를 적용합니다.',
  209. [EquipSlot.CommentEffect]: '내가 쓴 댓글에 배경·강조 효과를 적용합니다.'
  210. };
  211. // 슬롯 노출 순서 (아이템 관리 화면)
  212. export const EQUIP_SLOT_ORDER: EquipSlotValue[] = [EquipSlot.Badge, EquipSlot.NicknameEffect, EquipSlot.CommentEffect];