| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- // 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<ItemKindValue, string> = {
- [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<EquipSlotValue, string> = {
- [EquipSlot.Badge]: '배지',
- [EquipSlot.NicknameEffect]: '닉네임 이펙트',
- [EquipSlot.CommentEffect]: '댓글 이펙트'
- };
- export const EQUIP_SLOT_DESC: Record<EquipSlotValue, string> = {
- [EquipSlot.Badge]: '닉네임 옆에 표시되는 배지입니다.',
- [EquipSlot.NicknameEffect]: '닉네임에 색상·강조 효과를 적용합니다.',
- [EquipSlot.CommentEffect]: '내가 쓴 댓글에 배경·강조 효과를 적용합니다.'
- };
- // 슬롯 노출 순서 (아이템 관리 화면)
- export const EQUIP_SLOT_ORDER: EquipSlotValue[] = [EquipSlot.Badge, EquipSlot.NicknameEffect, EquipSlot.CommentEffect];
|