// D3 재화/보상/레벨/응원/지위아이템 도메인 타입 // Backend Application/Features/Api/{Rewards/GetToday, Member/GetLevel, Member/Items/*, Forum/PostCheer/*}/Response.cs 대응. // ⚠️ Web.Api Program.cs 에 JsonStringEnumConverter 미등록 → enum 은 전부 숫자로 직렬화됨 // (ItemKind·EquipSlot 은 정수 코드로 옴 — types/community.ts 와 동일 convention). // 라벨 맵은 이 파일 하단에 한 곳으로 모은다 (지시 6번 — ItemKind/EquipSlot 라벨 단일 소스). // ── 재화 (WalletBalanceType) — UI 이원화: 캐시 / 토큰 (d0 §2.2, Frontend/CLAUDE.md 용어 규칙) ── // 캐시 = PgCharged + Deposit + Adjusted (충전 재화, 상점 구매 전용) // 토큰 = Reward + Airdrop (활동 적립 재화, 응원·모의투자 전용) export const CASH_BALANCE_TYPES = ['PgCharged', 'Deposit', 'Adjusted'] as const; export const TOKEN_BALANCE_TYPES = ['Reward', 'Airdrop'] as const; // GET /api/wallet → { balances: [{ type, amount }] } export interface WalletBalanceRow { type: string; amount: number; } export interface WalletResponse { id: number; memberID: number; balance: number; balances: WalletBalanceRow[]; } // 캐시/토큰 합산 헬퍼 — balances 배열에서 파티션별 합계 export function sumCash(balances: WalletBalanceRow[]|undefined|null): number { if (!balances) { return 0; } return balances .filter(c => (CASH_BALANCE_TYPES as readonly string[]).includes(c.type)) .reduce((acc, c) => acc + c.amount, 0); } export function sumToken(balances: WalletBalanceRow[]|undefined|null): number { if (!balances) { return 0; } return balances .filter(c => (TOKEN_BALANCE_TYPES as readonly string[]).includes(c.type)) .reduce((acc, c) => acc + c.amount, 0); } // 응원 가용액 = 캐시 + 토큰 (Backend Wallet.GetCheerAvailable 대응 — 캐시→토큰 순 차감) export function sumCheerAvailable(balances: WalletBalanceRow[]|undefined|null): number { return sumCash(balances) + sumToken(balances); } // ── GET /api/members/me/level ── export interface MemberLevelResponse { exp: number; currentGradeID: number|null; currentGradeName: string|null; currentGradeImage: string|null; currentGradeTextColor: string|null; nextGradeID: number|null; nextGradeName: string|null; nextGradeRequiredExp: number|null; expToNextGrade: number|null; } // ── GET /api/rewards/me/today ── // ActionType 은 tinyint 숫자 (Post=1, Comment=2, LikeGiven=3, LikeReceived=4, Attendance=5, CheerReceived=6, Chat=7, PaperReward=8) export interface RewardActionSummary { actionType: number; actionName: string; count: number; expEarned: number; pointEarned: number; remainingCount: number|null; remainingExp: number|null; remainingPoint: number|null; } export interface RewardsTodayResponse { rewardDate: string; totalExp: number; totalPoint: number; actions: RewardActionSummary[]; } // ── GET /api/posts/{postID}/cheers ── export interface CheerItem { id: number; fromMemberID: number; fromNickname: string|null; amount: number; message: string|null; createdAt: string; } export interface CheerListResponse { totalAmount: number; count: number; recent: CheerItem[]; } // POST /api/posts/{postID}/cheers → { cheerID } export interface CheerSendRequest { amount: number; message?: string|null; } export interface CheerSendResponse { cheerID: number; } // ── ItemKind (Domain Store/ValueObject/ItemKind.cs — 숫자 직렬화) ── export const ItemKind = { Coupon: 1, Badge: 2, NicknameEffect: 3, CommentEffect: 4, ChatBanLift: 5, PostReset: 6, NotePass: 7, ExpBooster: 8, ProfileImageChange: 9, TaglineChange: 10, NicknameChange: 11 } as const; export type ItemKindValue = (typeof ItemKind)[keyof typeof ItemKind]; // ── EquipSlot (Domain Members/ValueObject/EquipSlot.cs — 숫자 직렬화) ── export const EquipSlot = { Badge: 1, NicknameEffect: 2, CommentEffect: 3 } as const; export type EquipSlotValue = (typeof EquipSlot)[keyof typeof EquipSlot]; // 장착형(지위) 아이템 종류 — 슬롯을 가지는 것들 export const STATUS_ITEM_KINDS: ItemKindValue[] = [ItemKind.Badge, ItemKind.NicknameEffect, ItemKind.CommentEffect]; // ── GET /api/members/me/items (List/Response.Row) ── // Kind·EquipSlot 은 숫자. isStatusItem = EquipSlot != null. export interface MemberItemRow { id: number; productID: number; productName: string; thumbnail: string|null; kind: ItemKindValue; equipSlot: EquipSlotValue|null; isEquipped: boolean; isStatusItem: boolean; effectPayload: string|null; durationDays: number; acquiredAt: string; expiresAt: string|null; usedAt: string|null; giftFromMemberID: number|null; giftFromName: string|null; } export interface MemberItemsResponse { total: number; list: MemberItemRow[]; } // POST /api/members/me/items/{id}/equip · /unequip → { id, slot, isEquipped } export interface EquipResponse { id: number; slot: EquipSlotValue; isEquipped: boolean; } // ── GET /api/members/equipped-items?ids= (Items/Equipped/Response) ── export interface EquippedItem { slot: EquipSlotValue; kind: ItemKindValue; productName: string; effectPayload: string|null; thumbnail: string|null; } export interface MemberEquip { memberID: number; items: EquippedItem[]; } export interface EquippedItemsResponse { members: MemberEquip[]; } // EffectPayload JSON — 색상·아이콘·애니메이션 키 (Product.EffectPayload 스냅샷) // 자유 형식이나 렌더에서 안전하게 참조할 필드만 선언 (없으면 무시). export interface EffectPayload { color?: string; background?: string; textColor?: string; icon?: string; animation?: string; label?: string; } export function parseEffectPayload(raw: string|null|undefined): EffectPayload|null { if (!raw) { return null; } try { const parsed = JSON.parse(raw); if (parsed && typeof parsed === 'object') { return parsed as EffectPayload; } } catch { // 잘못된 JSON 은 무시 (배지 미표시) } return null; } // ── 라벨 맵 (지시 6번 — ItemKind / EquipSlot 라벨 단일 소스) ── export const ITEM_KIND_LABEL: Record = { [ItemKind.Coupon]: '쿠폰', [ItemKind.Badge]: '배지', [ItemKind.NicknameEffect]: '닉네임 이펙트', [ItemKind.CommentEffect]: '댓글 이펙트', [ItemKind.ChatBanLift]: '채팅 밴 해제권', [ItemKind.PostReset]: '예전 글 초기화권', [ItemKind.NotePass]: '쪽지권', [ItemKind.ExpBooster]: '경험치 2배 모래시계', [ItemKind.ProfileImageChange]: '프로필 이미지 변경권', [ItemKind.TaglineChange]: '오늘의 한마디 변경권', [ItemKind.NicknameChange]: '닉네임 변경권' }; export const EQUIP_SLOT_LABEL: Record = { [EquipSlot.Badge]: '배지', [EquipSlot.NicknameEffect]: '닉네임 이펙트', [EquipSlot.CommentEffect]: '댓글 이펙트' }; export const EQUIP_SLOT_DESC: Record = { [EquipSlot.Badge]: '닉네임 옆에 표시되는 배지입니다.', [EquipSlot.NicknameEffect]: '닉네임에 색상·강조 효과를 적용합니다.', [EquipSlot.CommentEffect]: '내가 쓴 댓글에 배경·강조 효과를 적용합니다.' }; // 슬롯 노출 순서 (아이템 관리 화면) export const EQUIP_SLOT_ORDER: EquipSlotValue[] = [EquipSlot.Badge, EquipSlot.NicknameEffect, EquipSlot.CommentEffect];