| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117 |
- 'use client';
- // D3 응원(Cheer) 섹션 — 글 상세 하단. 누적 응원 배지 + 최근 목록 + 응원 버튼(+모달).
- // 목록 조회는 익명 허용(GET). 발신은 로그인 필요 + 본인 글에는 버튼 숨김.
- import { useCallback, useEffect, useState } from 'react';
- import { Heart } from 'lucide-react';
- import { fetchApi, getDateTime } from '@/lib/utils/client';
- import useAuth from '@/hooks/useAuth';
- import type { CheerListResponse } from '@/types/reward';
- import CheerModal from './CheerModal';
- type Props = {
- postID: number;
- authorMemberID: number|null;
- };
- export default function Cheer({ postID, authorMemberID }: Props)
- {
- const { member, loginCheck } = useAuth();
- const [data, setData] = useState<CheerListResponse|null>(null);
- const [open, setOpen] = useState(false);
- const [expanded, setExpanded] = useState(false);
- const isOwner = member?.id != null && member.id === authorMemberID;
- const load = useCallback(async () => {
- const res = await fetchApi<CheerListResponse>(`/api/posts/${postID}/cheers?take=20`, { silent: true });
- if (res.success && res.data) {
- setData(res.data);
- }
- }, [postID]);
- useEffect(() => {
- load();
- }, [load]);
- const handleOpen = useCallback(() => {
- if (!loginCheck()) {
- return;
- }
- setOpen(true);
- }, [loginCheck]);
- const handleDone = useCallback(() => {
- setOpen(false);
- load();
- }, [load]);
- const total = data?.totalAmount ?? 0;
- const count = data?.count ?? 0;
- const recent = data?.recent ?? [];
- const shown = expanded ? recent : recent.slice(0, 5);
- return (
- <section className='my-4 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-4'>
- <div className='flex flex-wrap items-center justify-between gap-3'>
- <div className='flex items-center gap-2'>
- <Heart className='size-5 text-red-500' aria-hidden />
- <span className='font-semibold'>응원</span>
- {count > 0 && (
- <span className='inline-flex items-center gap-1 rounded-full bg-red-50 dark:bg-red-950/40 px-2.5 py-1 text-sm text-red-600 dark:text-red-400 font-semibold'>
- {total.toLocaleString()}
- <span className='text-xs font-normal text-red-400'>({count.toLocaleString()}건)</span>
- </span>
- )}
- </div>
- {!isOwner && (
- <button
- type='button'
- onClick={handleOpen}
- className='inline-flex items-center gap-1.5 px-4 py-2 rounded-md bg-red-600 text-white text-sm font-semibold hover:bg-red-700'
- >
- <Heart className='size-4' aria-hidden />
- 응원하기
- </button>
- )}
- </div>
- {isOwner && count === 0 && (
- <p className='mt-2 text-xs text-neutral-400'>내 글에는 응원할 수 없습니다.</p>
- )}
- {recent.length > 0 && (
- <ul className='mt-3 flex flex-col gap-2'>
- {shown.map((c) => (
- <li key={c.id} className='flex items-start gap-2 text-sm'>
- <span className='inline-flex shrink-0 items-center rounded bg-red-50 dark:bg-red-950/40 px-1.5 py-0.5 text-xs font-semibold text-red-600 dark:text-red-400'>
- {c.amount.toLocaleString()}
- </span>
- <div className='min-w-0 flex-1'>
- <span className='font-semibold'>{c.fromNickname ?? '익명'}</span>
- {c.message && <span className='ml-1.5 text-neutral-600 dark:text-neutral-300 break-words'>{c.message}</span>}
- </div>
- <span className='shrink-0 text-xs text-neutral-400'>{getDateTime(c.createdAt)}</span>
- </li>
- ))}
- </ul>
- )}
- {recent.length > 5 && (
- <button
- type='button'
- onClick={() => setExpanded((prev) => !prev)}
- className='mt-2 text-xs text-neutral-500 hover:underline'
- >
- {expanded ? '접기' : `최근 응원 더보기 (${recent.length})`}
- </button>
- )}
- {open && (
- <CheerModal postID={postID} onClose={() => setOpen(false)} onDone={handleDone} />
- )}
- </section>
- );
- }
|