feat(WEB): 동적 게시판, 파트너 관리, 공지 팝업 모달 추가
- 동적 게시판 시스템 구현 (/boards/[boardCode]) - 파트너 관리 페이지 및 폼 추가 - 공지 팝업 모달 컴포넌트 (NoticePopupModal) - localStorage 기반 1일간 숨김 기능 - 테스트 페이지 (/test/popup) - IntegratedListTemplateV2 개선 - 기타 버그 수정 및 타입 개선 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
169
src/components/common/NoticePopupModal/NoticePopupModal.tsx
Normal file
169
src/components/common/NoticePopupModal/NoticePopupModal.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/components/ui/utils';
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
|
||||
// ============================================
|
||||
// 타입 정의
|
||||
// ============================================
|
||||
|
||||
export interface NoticePopupData {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
interface NoticePopupModalProps {
|
||||
popup: NoticePopupData;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// localStorage 유틸리티
|
||||
// ============================================
|
||||
|
||||
const POPUP_DISMISS_PREFIX = 'popup_dismissed_';
|
||||
|
||||
/**
|
||||
* 자정까지의 타임스탬프 계산
|
||||
*/
|
||||
function getMidnightTimestamp(): number {
|
||||
const now = new Date();
|
||||
const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0, 0);
|
||||
return midnight.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* 팝업이 오늘 하루 동안 숨김 상태인지 확인
|
||||
*/
|
||||
export function isPopupDismissedForToday(popupId: string): boolean {
|
||||
if (typeof window === 'undefined') return false;
|
||||
|
||||
const dismissedUntil = localStorage.getItem(`${POPUP_DISMISS_PREFIX}${popupId}`);
|
||||
if (!dismissedUntil) return false;
|
||||
|
||||
const dismissedTimestamp = parseInt(dismissedUntil, 10);
|
||||
return Date.now() < dismissedTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 팝업을 오늘 하루 동안 숨김 처리
|
||||
*/
|
||||
function dismissPopupForToday(popupId: string): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const midnightTimestamp = getMidnightTimestamp();
|
||||
localStorage.setItem(`${POPUP_DISMISS_PREFIX}${popupId}`, String(midnightTimestamp));
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 컴포넌트
|
||||
// ============================================
|
||||
|
||||
export function NoticePopupModal({ popup, open, onOpenChange }: NoticePopupModalProps) {
|
||||
const [dontShowToday, setDontShowToday] = React.useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
if (dontShowToday) {
|
||||
dismissPopupForToday(popup.id);
|
||||
}
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
|
||||
<DialogPrimitive.Portal>
|
||||
{/* 오버레이 */}
|
||||
<DialogPrimitive.Overlay
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/50',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
||||
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0'
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 팝업 컨텐츠 */}
|
||||
<DialogPrimitive.Content
|
||||
className={cn(
|
||||
'fixed top-[50%] left-[50%] z-50 translate-x-[-50%] translate-y-[-50%]',
|
||||
'w-full max-w-md bg-background rounded-lg shadow-lg overflow-hidden',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
||||
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
||||
'duration-200'
|
||||
)}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="bg-muted px-4 py-3 border-b">
|
||||
<DialogPrimitive.Title className="text-base font-medium text-center">
|
||||
알림
|
||||
</DialogPrimitive.Title>
|
||||
</div>
|
||||
|
||||
{/* 본문 */}
|
||||
<div className="p-6">
|
||||
{/* 제목 */}
|
||||
<h3 className="text-base font-medium mb-4">{popup.title}</h3>
|
||||
|
||||
{/* 이미지 영역 */}
|
||||
{popup.imageUrl ? (
|
||||
<div className="relative w-full aspect-[4/3] mb-4 rounded-md overflow-hidden border bg-muted">
|
||||
<img
|
||||
src={popup.imageUrl}
|
||||
alt={popup.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full aspect-[4/3] mb-4 rounded-md border bg-muted flex items-center justify-center">
|
||||
<span className="text-muted-foreground text-sm">IMG</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 내용 */}
|
||||
<div className="text-sm text-foreground mb-6">
|
||||
<p className="text-muted-foreground mb-2">내용</p>
|
||||
<div
|
||||
className="prose prose-sm max-w-none"
|
||||
dangerouslySetInnerHTML={{ __html: popup.content }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 구분선 */}
|
||||
<div className="border-t mb-4" />
|
||||
|
||||
{/* 하단 영역 */}
|
||||
<div className="flex items-center justify-between">
|
||||
{/* 체크박스 */}
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={dontShowToday}
|
||||
onCheckedChange={(checked) => setDontShowToday(checked === true)}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
1일간 이 창을 열지 않음
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{/* 닫기 버튼 */}
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={handleClose}
|
||||
className="px-6"
|
||||
>
|
||||
닫기
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Portal>
|
||||
</DialogPrimitive.Root>
|
||||
);
|
||||
}
|
||||
2
src/components/common/NoticePopupModal/index.ts
Normal file
2
src/components/common/NoticePopupModal/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { NoticePopupModal, isPopupDismissedForToday } from './NoticePopupModal';
|
||||
export type { NoticePopupData } from './NoticePopupModal';
|
||||
Reference in New Issue
Block a user