- types.ts: Mock 데이터 및 camelCase 타입 삭제, swagger 기준 snake_case 타입 추가 - message.api.ts: 신규 생성 (목록/상세/저장/삭제/검증 API 함수) - MessageListPage: MOCK_MESSAGES → fetchMessages API, 서비스 필터 fetchServices로 실제 로드 - MessageSlidePanel: MOCK_MESSAGE_DETAILS → fetchMessageInfo API, deleteMessage API 연동 - MessageRegisterPage: SERVICE_OPTIONS → fetchServices API, validateMessage → saveMessage 흐름 - MessageRegisterPage: 서비스 선택을 FilterDropdown 스타일 커스텀 드롭다운으로 변경 - MessagePreview: 빈 내용 시 플레이스홀더 텍스트 제거 Closes #35
380 lines
16 KiB
TypeScript
380 lines
16 KiB
TypeScript
import { useState } from "react";
|
|
|
|
interface MessagePreviewProps {
|
|
title: string;
|
|
body: string;
|
|
hasImage: boolean;
|
|
appName?: string;
|
|
variant?: "large" | "small";
|
|
}
|
|
|
|
// 폰 배경 이미지 (밝은 그래디언트 배경)
|
|
const PHONE_BG =
|
|
"https://lh3.googleusercontent.com/aida-public/AB6AXuDmbD5msJ9uegWOcy0256wH6JsipGzrgtab3foEKiGVs_a4SbUCTPti6BVDJOQEP4ZCvbcAw9hI3C7QuUdPxrBf3jJm3VgKkWoqSzl--ZEbPIzimbYnM1HQEsRbil7nmWG_XscwPP30V3OFnyleVY_R7Urk0UYbrL8P1OJwW1xwYfBDJv4htBuICd9GR2NIJlSShaBxfF9Kgp59Cte3VapdHxCz9p2Cb9tf1t13xc2LV348V-kfyQNtL8XCZNP3LMrrUIR4SrV3cGM";
|
|
|
|
export default function MessagePreview({
|
|
title,
|
|
body,
|
|
hasImage,
|
|
appName = "SPMS",
|
|
variant = "large",
|
|
}: MessagePreviewProps) {
|
|
const [tab, setTab] = useState<"ios" | "android">("ios");
|
|
|
|
const isLarge = variant === "large";
|
|
const phoneWidth = isLarge ? "w-[300px]" : "w-[240px]";
|
|
const truncatedBody =
|
|
body.length > 50 ? body.substring(0, 50) + "..." : body;
|
|
|
|
return (
|
|
<div>
|
|
{/* 탭 */}
|
|
<div className="flex border-b border-gray-200 mb-4">
|
|
<button
|
|
type="button"
|
|
className={`flex-1 px-3 py-2 text-xs font-medium border-b-2 transition-colors cursor-pointer ${
|
|
tab === "ios"
|
|
? "text-[#0f172a] border-[#2563EB]"
|
|
: "text-gray-400 border-transparent hover:text-[#0f172a]"
|
|
}`}
|
|
onClick={() => setTab("ios")}
|
|
>
|
|
iOS
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={`flex-1 px-3 py-2 text-xs font-medium border-b-2 transition-colors cursor-pointer ${
|
|
tab === "android"
|
|
? "text-[#0f172a] border-[#2563EB]"
|
|
: "text-gray-400 border-transparent hover:text-[#0f172a]"
|
|
}`}
|
|
onClick={() => setTab("android")}
|
|
>
|
|
Android
|
|
</button>
|
|
</div>
|
|
|
|
{/* iOS 프리뷰 */}
|
|
{tab === "ios" && (
|
|
<div className="flex flex-col gap-5">
|
|
{/* Banner Notification */}
|
|
<div className="min-h-[130px]">
|
|
<p className="text-[10px] uppercase font-bold text-gray-400 tracking-wider mb-2">
|
|
Banner Notification
|
|
</p>
|
|
<div className="bg-white/90 backdrop-blur-md rounded-2xl p-3 shadow-lg border border-gray-200">
|
|
<div className="flex items-center gap-2.5">
|
|
<div className="size-8 bg-[#2563EB] rounded-xl flex items-center justify-center flex-shrink-0 shadow-sm">
|
|
<span
|
|
className="material-symbols-outlined text-white"
|
|
style={{ fontSize: "16px" }}
|
|
>
|
|
shopping_bag
|
|
</span>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-xs font-bold text-[#0f172a] break-words">
|
|
{title || "메시지 제목을 입력하세요"}
|
|
</p>
|
|
<p className="text-[10px] text-gray-600 mt-0.5 break-words">
|
|
{body}
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-col items-end gap-1 flex-shrink-0 self-start">
|
|
<p className="text-[10px] text-gray-400">now</p>
|
|
{hasImage && (
|
|
<div className="w-8 h-8 bg-gray-100 border border-dashed border-gray-300 rounded flex items-center justify-center">
|
|
<span
|
|
className="material-symbols-outlined text-gray-300"
|
|
style={{ fontSize: "14px" }}
|
|
>
|
|
image
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Full Screen Preview - iOS */}
|
|
<div>
|
|
<p className="text-[10px] uppercase font-bold text-gray-400 tracking-wider mb-2">
|
|
Full Screen Preview
|
|
</p>
|
|
<div className="flex justify-center">
|
|
<div className={`relative ${phoneWidth}`}>
|
|
<div
|
|
className={`${isLarge ? "border-[10px]" : "border-[8px]"} border-gray-800 bg-gray-800 ${isLarge ? "rounded-[2.5rem]" : "rounded-[2rem]"} overflow-hidden shadow-xl`}
|
|
style={{ aspectRatio: "9/19.5" }}
|
|
>
|
|
{/* 노치 */}
|
|
<div
|
|
className={`absolute top-0 left-1/2 -translate-x-1/2 ${isLarge ? "w-32 h-7" : "w-24 h-5"} bg-black rounded-b-2xl z-10`}
|
|
/>
|
|
<div
|
|
className="w-full h-full bg-cover bg-center overflow-hidden relative"
|
|
style={{
|
|
backgroundImage: `url('${PHONE_BG}')`,
|
|
filter: "brightness(1)",
|
|
}}
|
|
>
|
|
{/* 상태바 */}
|
|
<div className="px-4 pt-2 pb-1 flex justify-between items-center text-white text-[10px] font-medium bg-black/20">
|
|
<span>9:41</span>
|
|
<div className="flex gap-0.5 items-center">
|
|
<span
|
|
className="material-symbols-outlined"
|
|
style={{ fontSize: "12px" }}
|
|
>
|
|
signal_cellular_alt
|
|
</span>
|
|
<span
|
|
className="material-symbols-outlined"
|
|
style={{ fontSize: "12px" }}
|
|
>
|
|
wifi
|
|
</span>
|
|
<span
|
|
className="material-symbols-outlined"
|
|
style={{ fontSize: "12px" }}
|
|
>
|
|
battery_full
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{/* 알림 카드 */}
|
|
<div
|
|
className={`absolute ${isLarge ? "top-16 left-3 right-3" : "top-12 left-2 right-2"} bg-white/80 backdrop-blur-md rounded-lg shadow-lg border border-white/30 overflow-hidden`}
|
|
>
|
|
<div className="p-2.5">
|
|
<div className="flex items-center gap-2">
|
|
<div className="size-6 bg-[#2563EB] rounded-md flex items-center justify-center flex-shrink-0">
|
|
<span
|
|
className="material-symbols-outlined text-white"
|
|
style={{ fontSize: "12px" }}
|
|
>
|
|
shopping_bag
|
|
</span>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-[10px] font-bold text-[#0f172a] break-words">
|
|
{title || "메시지 제목을 입력하세요"}
|
|
</p>
|
|
<p className="text-[8px] text-gray-600 mt-0.5 break-words">
|
|
{truncatedBody}
|
|
</p>
|
|
</div>
|
|
<p className="text-[8px] text-gray-400 flex-shrink-0 self-start">
|
|
now
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{hasImage && (
|
|
<div
|
|
className="w-full bg-gray-100 border-t border-dashed border-gray-200 flex items-center justify-center"
|
|
style={{ height: isLarge ? 150 : 120 }}
|
|
>
|
|
<span className="material-symbols-outlined text-gray-300 text-2xl">
|
|
image
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{/* 홈 인디케이터 */}
|
|
<div className="absolute bottom-0 left-0 right-0 h-5 flex items-center justify-center">
|
|
<div className="w-24 h-1 bg-white/60 rounded-full" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Android 프리뷰 */}
|
|
{tab === "android" && (
|
|
<div className="flex flex-col gap-5">
|
|
{/* Banner Notification */}
|
|
<div className="min-h-[130px]">
|
|
<p className="text-[10px] uppercase font-bold text-gray-400 tracking-wider mb-2">
|
|
Banner Notification
|
|
</p>
|
|
<div className="bg-white rounded-2xl p-3 shadow-lg border border-gray-200">
|
|
<div className="flex items-center gap-1.5 mb-1.5">
|
|
<div className="size-3.5 bg-[#2563EB] rounded-full flex items-center justify-center flex-shrink-0">
|
|
<span
|
|
className="material-symbols-outlined text-white"
|
|
style={{ fontSize: "8px" }}
|
|
>
|
|
shopping_bag
|
|
</span>
|
|
</div>
|
|
<p className="text-[10px] text-gray-500 font-medium">{appName}</p>
|
|
<span className="text-[10px] text-gray-300 mx-0.5">·</span>
|
|
<p className="text-[10px] text-gray-400">now</p>
|
|
</div>
|
|
<div className="flex items-center gap-2.5">
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-xs font-bold text-[#0f172a] break-words">
|
|
{title || "메시지 제목을 입력하세요"}
|
|
</p>
|
|
<p className="text-[10px] text-gray-600 mt-0.5 break-words">
|
|
{body}
|
|
</p>
|
|
</div>
|
|
{hasImage && (
|
|
<div className="w-8 h-8 bg-gray-100 border border-dashed border-gray-300 rounded flex items-center justify-center flex-shrink-0">
|
|
<span
|
|
className="material-symbols-outlined text-gray-300"
|
|
style={{ fontSize: "14px" }}
|
|
>
|
|
image
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Full Screen Preview - Android */}
|
|
<div>
|
|
<p className="text-[10px] uppercase font-bold text-gray-400 tracking-wider mb-2">
|
|
Full Screen Preview
|
|
</p>
|
|
<div className="flex justify-center">
|
|
<div className={`relative ${phoneWidth}`}>
|
|
<div
|
|
className={`${isLarge ? "border-[10px]" : "border-[8px]"} border-gray-800 bg-gray-800 rounded-[2rem] overflow-hidden shadow-xl`}
|
|
style={{ aspectRatio: "9/19.5" }}
|
|
>
|
|
<div
|
|
className="w-full h-full bg-cover bg-center overflow-hidden relative"
|
|
style={{
|
|
backgroundImage: `url('${PHONE_BG}')`,
|
|
filter: "brightness(1)",
|
|
}}
|
|
>
|
|
{/* 상태바 */}
|
|
<div className="px-4 pt-2 pb-1 flex justify-between items-center text-white text-[10px] font-medium bg-black/20">
|
|
<span>12:30</span>
|
|
<div className="flex gap-0.5 items-center">
|
|
<span
|
|
className="material-symbols-outlined"
|
|
style={{ fontSize: "12px" }}
|
|
>
|
|
signal_cellular_alt
|
|
</span>
|
|
<span
|
|
className="material-symbols-outlined"
|
|
style={{ fontSize: "12px" }}
|
|
>
|
|
wifi
|
|
</span>
|
|
<span
|
|
className="material-symbols-outlined"
|
|
style={{ fontSize: "12px" }}
|
|
>
|
|
battery_full
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{/* 알림 카드 */}
|
|
<div
|
|
className={`absolute ${isLarge ? "top-14" : "top-11"} left-2 right-2 bg-white/90 rounded-xl shadow-lg overflow-hidden`}
|
|
>
|
|
<div className="px-2.5 pt-2 pb-1.5">
|
|
<div className="flex items-center gap-1 mb-1">
|
|
<div className="size-3 bg-[#2563EB] rounded-full flex items-center justify-center flex-shrink-0">
|
|
<span
|
|
className="material-symbols-outlined text-white"
|
|
style={{ fontSize: "7px" }}
|
|
>
|
|
shopping_bag
|
|
</span>
|
|
</div>
|
|
<p className="text-[8px] text-gray-500 font-medium">
|
|
{appName}
|
|
</p>
|
|
<span className="text-[8px] text-gray-300 mx-0.5">
|
|
·
|
|
</span>
|
|
<p className="text-[8px] text-gray-400">now</p>
|
|
<span
|
|
className="material-symbols-outlined text-gray-400 ml-auto"
|
|
style={{ fontSize: "10px" }}
|
|
>
|
|
expand_more
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-[10px] font-bold text-[#0f172a] break-words">
|
|
{title || "메시지 제목을 입력하세요"}
|
|
</p>
|
|
<p className="text-[8px] text-gray-600 mt-0.5 break-words">
|
|
{truncatedBody}
|
|
</p>
|
|
</div>
|
|
{hasImage && (
|
|
<div className="w-8 h-8 bg-gray-100 border border-dashed border-gray-200 rounded flex items-center justify-center flex-shrink-0">
|
|
<span
|
|
className="material-symbols-outlined text-gray-300"
|
|
style={{ fontSize: "12px" }}
|
|
>
|
|
image
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{hasImage && (
|
|
<div
|
|
className="w-full bg-gray-100 border-t border-dashed border-gray-200 flex items-center justify-center"
|
|
style={{ height: isLarge ? 150 : 120 }}
|
|
>
|
|
<span className="material-symbols-outlined text-gray-300 text-2xl">
|
|
image
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{/* 네비게이션 바 */}
|
|
<div className="absolute bottom-0 left-0 right-0 h-4 flex items-center justify-center gap-10 bg-black/10">
|
|
<span
|
|
className="material-symbols-outlined text-white/60"
|
|
style={{ fontSize: "13px" }}
|
|
>
|
|
arrow_back
|
|
</span>
|
|
<span
|
|
className="material-symbols-outlined text-white/60"
|
|
style={{ fontSize: "13px" }}
|
|
>
|
|
circle
|
|
</span>
|
|
<span
|
|
className="material-symbols-outlined text-white/60"
|
|
style={{ fontSize: "13px" }}
|
|
>
|
|
crop_square
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<p className="text-[10px] text-gray-400 leading-relaxed text-center mt-4">
|
|
이 미리보기는 예상 디자인입니다.
|
|
<br />
|
|
실제 표시는 기기 설정 및 OS 버전에 따라 다를 수 있습니다.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|