From 2549930a5a1b4dc52078f92750ba96c4f4d07343 Mon Sep 17 00:00:00 2001 From: SEAN Date: Mon, 2 Mar 2026 09:51:02 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20API=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- react/src/api/message.api.ts | 58 +++++ .../message/components/MessagePreview.tsx | 8 +- .../message/components/MessageSlidePanel.tsx | 117 +++++++--- .../message/pages/MessageListPage.tsx | 173 ++++++++++----- .../message/pages/MessageRegisterPage.tsx | 159 +++++++++++--- react/src/features/message/types.ts | 201 +++++++----------- 6 files changed, 470 insertions(+), 246 deletions(-) create mode 100644 react/src/api/message.api.ts diff --git a/react/src/api/message.api.ts b/react/src/api/message.api.ts new file mode 100644 index 0000000..87d904a --- /dev/null +++ b/react/src/api/message.api.ts @@ -0,0 +1,58 @@ +import { apiClient } from "./client"; +import type { ApiResponse } from "@/types/api"; +import type { + MessageListRequest, + MessageListResponse, + MessageInfoRequest, + MessageInfoResponse, + MessageSaveRequest, + MessageDeleteRequest, + MessageValidateRequest, +} from "@/features/message/types"; + +/** 메시지 목록 조회 */ +export function fetchMessages(data: MessageListRequest) { + return apiClient.post>( + "/v1/in/message/list", + data, + ); +} + +/** 메시지 상세 조회 */ +export function fetchMessageInfo( + data: MessageInfoRequest, + serviceCode: string, +) { + return apiClient.post>( + "/v1/in/message/info", + data, + { headers: { "X-Service-Code": serviceCode } }, + ); +} + +/** 메시지 저장 */ +export function saveMessage(data: MessageSaveRequest, serviceCode: string) { + return apiClient.post>("/v1/in/message/save", data, { + headers: { "X-Service-Code": serviceCode }, + }); +} + +/** 메시지 삭제 */ +export function deleteMessage( + data: MessageDeleteRequest, + serviceCode: string, +) { + return apiClient.post>("/v1/in/message/delete", data, { + headers: { "X-Service-Code": serviceCode }, + }); +} + +/** 메시지 검증 */ +export function validateMessage( + data: MessageValidateRequest, + serviceCode: string, +) { + return apiClient.post>("/v1/in/message/validate", data, { + headers: { "X-Service-Code": serviceCode }, + }); +} diff --git a/react/src/features/message/components/MessagePreview.tsx b/react/src/features/message/components/MessagePreview.tsx index 0112f96..6c63d7d 100644 --- a/react/src/features/message/components/MessagePreview.tsx +++ b/react/src/features/message/components/MessagePreview.tsx @@ -77,7 +77,7 @@ export default function MessagePreview({ {title || "메시지 제목을 입력하세요"}

- {body || "메시지 내용을 입력하세요"} + {body}

@@ -162,7 +162,7 @@ export default function MessagePreview({ {title || "메시지 제목을 입력하세요"}

- {truncatedBody || "메시지 내용을 입력하세요"} + {truncatedBody}

@@ -221,7 +221,7 @@ export default function MessagePreview({ {title || "메시지 제목을 입력하세요"}

- {body || "메시지 내용을 입력하세요"} + {body}

{hasImage && ( @@ -314,7 +314,7 @@ export default function MessagePreview({ {title || "메시지 제목을 입력하세요"}

- {truncatedBody || "메시지 내용을 입력하세요"} + {truncatedBody}

{hasImage && ( diff --git a/react/src/features/message/components/MessageSlidePanel.tsx b/react/src/features/message/components/MessageSlidePanel.tsx index f210110..d881b2c 100644 --- a/react/src/features/message/components/MessageSlidePanel.tsx +++ b/react/src/features/message/components/MessageSlidePanel.tsx @@ -2,29 +2,57 @@ import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import CopyButton from "@/components/common/CopyButton"; import MessagePreview from "./MessagePreview"; -import { MOCK_MESSAGE_DETAILS } from "../types"; +import { fetchMessageInfo, deleteMessage } from "@/api/message.api"; +import type { MessageInfoResponse } from "../types"; interface MessageSlidePanelProps { isOpen: boolean; onClose: () => void; - messageId: string | null; + messageCode: string | null; + serviceCode: string | null; } export default function MessageSlidePanel({ isOpen, onClose, - messageId, + messageCode, + serviceCode, }: MessageSlidePanelProps) { - const detail = messageId ? MOCK_MESSAGE_DETAILS[messageId] : null; + const [detail, setDetail] = useState(null); + const [loading, setLoading] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [deleting, setDeleting] = useState(false); const bodyRef = useRef(null); + // 메시지 상세 조회 + useEffect(() => { + if (!isOpen || !messageCode || !serviceCode) { + setDetail(null); + return; + } + (async () => { + setLoading(true); + try { + const res = await fetchMessageInfo( + { message_code: messageCode }, + serviceCode, + ); + setDetail(res.data.data); + } catch { + setDetail(null); + toast.error("메시지 상세 조회에 실패했습니다."); + } finally { + setLoading(false); + } + })(); + }, [isOpen, messageCode, serviceCode]); + // 패널 열릴 때 스크롤 최상단으로 리셋 useEffect(() => { if (isOpen) { bodyRef.current?.scrollTo(0, 0); } - }, [isOpen, messageId]); + }, [isOpen, messageCode]); // ESC 닫기 useEffect(() => { @@ -47,6 +75,33 @@ export default function MessageSlidePanel({ }; }, [isOpen]); + // 삭제 처리 + const handleDelete = async () => { + if (!detail?.message_code || !serviceCode) return; + setDeleting(true); + try { + await deleteMessage( + { message_code: detail.message_code }, + serviceCode, + ); + toast.success(`${detail.message_code} 메시지가 삭제되었습니다.`); + setShowDeleteConfirm(false); + onClose(); + } catch { + toast.error("메시지 삭제에 실패했습니다."); + } finally { + setDeleting(false); + } + }; + + // 기타 정보 문자열 변환 + const extraText = + detail?.data != null + ? typeof detail.data === "string" + ? detail.data + : JSON.stringify(detail.data, null, 2) + : ""; + return ( <> {/* 오버레이 */} @@ -76,7 +131,17 @@ export default function MessageSlidePanel({ {/* 패널 본문 */}
- {detail ? ( + {loading ? ( + /* 로딩 스켈레톤 */ +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+
+
+
+ ))} +
+ ) : detail ? (
{/* 메시지 ID */}
@@ -84,9 +149,9 @@ export default function MessageSlidePanel({ 메시지 ID - {detail.messageId} + {detail.message_code} - +
@@ -97,7 +162,7 @@ export default function MessageSlidePanel({ 서비스 선택

- {detail.serviceName} + {detail.service_name}

@@ -128,12 +193,12 @@ export default function MessageSlidePanel({

- {detail.imageUrl || "등록된 이미지 없음"} + {detail.image_url || "등록된 이미지 없음"}

@@ -145,16 +210,16 @@ export default function MessageSlidePanel({ - {detail.linkType === "deeplink" ? "딥링크" : "웹 링크"} + {detail.link_type === "deeplink" ? "딥링크" : "웹 링크"}

- {detail.linkUrl || "—"} + {detail.link_url || "—"}

@@ -164,7 +229,7 @@ export default function MessageSlidePanel({ 기타 정보
- {detail.extra || "—"} + {extraText || "—"}
@@ -176,10 +241,10 @@ export default function MessageSlidePanel({ 프리뷰 @@ -220,7 +285,7 @@ export default function MessageSlidePanel({

메시지 삭제

- {detail.messageId} 메시지를 삭제하시겠습니까? + {detail.message_code} 메시지를 삭제하시겠습니까?

@@ -236,22 +301,20 @@ export default function MessageSlidePanel({
diff --git a/react/src/features/message/pages/MessageListPage.tsx b/react/src/features/message/pages/MessageListPage.tsx index e76119f..4824aab 100644 --- a/react/src/features/message/pages/MessageListPage.tsx +++ b/react/src/features/message/pages/MessageListPage.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useEffect } from "react"; +import { useState, useCallback, useEffect } from "react"; import { Link, useSearchParams } from "react-router-dom"; import PageHeader from "@/components/common/PageHeader"; import SearchInput from "@/components/common/SearchInput"; @@ -9,7 +9,9 @@ import EmptyState from "@/components/common/EmptyState"; import CopyButton from "@/components/common/CopyButton"; import MessageSlidePanel from "../components/MessageSlidePanel"; import { formatDate } from "@/utils/format"; -import { MOCK_MESSAGES, SERVICE_FILTER_OPTIONS } from "../types"; +import { fetchMessages } from "@/api/message.api"; +import { fetchServices } from "@/api/service.api"; +import type { MessageListItem } from "../types"; const PAGE_SIZE = 10; @@ -26,6 +28,84 @@ export default function MessageListPage() { const [appliedSearch, setAppliedSearch] = useState(""); const [appliedService, setAppliedService] = useState("전체 서비스"); + // 데이터 상태 + const [items, setItems] = useState([]); + const [totalItems, setTotalItems] = useState(0); + const [totalPages, setTotalPages] = useState(1); + + // 서비스 필터 옵션 + const [serviceFilterOptions, setServiceFilterOptions] = useState([ + "전체 서비스", + ]); + const [serviceCodeMap, setServiceCodeMap] = useState< + Record + >({}); + + // 슬라이드 패널 상태 + const [panelOpen, setPanelOpen] = useState(false); + const [selectedMessageCode, setSelectedMessageCode] = useState( + null, + ); + const [selectedServiceCode, setSelectedServiceCode] = useState( + null, + ); + + // 서비스 목록 로드 + useEffect(() => { + (async () => { + try { + const res = await fetchServices({ page: 1, pageSize: 100 }); + const svcItems = res.data.data.items ?? []; + const names = svcItems.map((s) => s.serviceName); + setServiceFilterOptions(["전체 서비스", ...names]); + const codeMap: Record = {}; + svcItems.forEach((s) => { + codeMap[s.serviceName] = s.serviceCode; + }); + setServiceCodeMap(codeMap); + } catch { + // 서비스 목록 로드 실패 시 기본값 유지 + } + })(); + }, []); + + // 데이터 로드 + const loadData = useCallback( + async (page: number, keyword: string, serviceName: string) => { + setLoading(true); + try { + // 서비스명 → 서비스코드 변환 + const serviceCode = + serviceName !== "전체 서비스" + ? serviceCodeMap[serviceName] || undefined + : undefined; + + const res = await fetchMessages({ + page, + size: PAGE_SIZE, + keyword: keyword || undefined, + service_code: serviceCode, + }); + const data = res.data.data; + setItems(data.items ?? []); + setTotalItems(data.totalCount ?? 0); + setTotalPages(data.totalPages ?? 1); + } catch { + setItems([]); + setTotalItems(0); + setTotalPages(1); + } finally { + setLoading(false); + } + }, + [serviceCodeMap], + ); + + // 초기 로드 + useEffect(() => { + loadData(1, "", "전체 서비스"); + }, [loadData]); + // URL 쿼리 파라미터로 messageId가 넘어온 경우 자동 검색 useEffect(() => { const messageId = searchParams.get("messageId"); @@ -33,24 +113,16 @@ export default function MessageListPage() { setSearch(messageId); setAppliedSearch(messageId); setSearchParams({}, { replace: true }); + loadData(1, messageId, "전체 서비스"); } }, []); // eslint-disable-line react-hooks/exhaustive-deps - // 슬라이드 패널 상태 - const [panelOpen, setPanelOpen] = useState(false); - const [selectedMessageId, setSelectedMessageId] = useState( - null - ); - // 조회 버튼 const handleQuery = () => { - setLoading(true); - setTimeout(() => { - setAppliedSearch(search); - setAppliedService(serviceFilter); - setCurrentPage(1); - setLoading(false); - }, 400); + setAppliedSearch(search); + setAppliedService(serviceFilter); + setCurrentPage(1); + loadData(1, search, serviceFilter); }; // 필터 초기화 @@ -60,41 +132,19 @@ export default function MessageListPage() { setAppliedSearch(""); setAppliedService("전체 서비스"); setCurrentPage(1); + loadData(1, "", "전체 서비스"); }; - // 필터링된 데이터 - const filtered = useMemo(() => { - return MOCK_MESSAGES.filter((msg) => { - // 검색 (메시지 ID / 제목) - if (appliedSearch) { - const q = appliedSearch.toLowerCase(); - if ( - !msg.messageId.toLowerCase().includes(q) && - !msg.title.toLowerCase().includes(q) - ) return false; - } - // 서비스 필터 - if ( - appliedService !== "전체 서비스" && - msg.serviceName !== appliedService - ) { - return false; - } - return true; - }); - }, [appliedSearch, appliedService]); - - // 페이지네이션 - const totalItems = filtered.length; - const totalPages = Math.max(1, Math.ceil(totalItems / PAGE_SIZE)); - const paged = filtered.slice( - (currentPage - 1) * PAGE_SIZE, - currentPage * PAGE_SIZE - ); + // 페이지 변경 + const handlePageChange = (page: number) => { + setCurrentPage(page); + loadData(page, appliedSearch, appliedService); + }; // 행 클릭 → 슬라이드 패널 - const handleRowClick = (messageId: string) => { - setSelectedMessageId(messageId); + const handleRowClick = (item: MessageListItem) => { + setSelectedMessageCode(item.message_code); + setSelectedServiceCode(item.service_code); setPanelOpen(true); }; @@ -127,7 +177,7 @@ export default function MessageListPage() {
- ) : paged.length > 0 ? ( + ) : items.length > 0 ? (
@@ -208,11 +258,11 @@ export default function MessageListPage() { - {paged.map((msg, idx) => ( + {items.map((msg, idx) => ( handleRowClick(msg.messageId)} + key={msg.message_code ?? idx} + className={`${idx < items.length - 1 ? "border-b border-gray-100" : ""} hover:bg-gray-50/50 transition-colors cursor-pointer`} + onClick={() => handleRowClick(msg)} > {/* 메시지 ID */} {/* 메시지 제목 */} @@ -235,13 +285,13 @@ export default function MessageListPage() { {/* 서비스 */} {/* 작성일 */} @@ -256,7 +306,7 @@ export default function MessageListPage() { totalPages={totalPages} totalItems={totalItems} pageSize={PAGE_SIZE} - onPageChange={setCurrentPage} + onPageChange={handlePageChange} /> ) : ( @@ -270,8 +320,13 @@ export default function MessageListPage() { {/* 슬라이드 패널 */} setPanelOpen(false)} - messageId={selectedMessageId} + onClose={() => { + setPanelOpen(false); + // 삭제 후 목록 새로고침 + loadData(currentPage, appliedSearch, appliedService); + }} + messageCode={selectedMessageCode} + serviceCode={selectedServiceCode} /> ); diff --git a/react/src/features/message/pages/MessageRegisterPage.tsx b/react/src/features/message/pages/MessageRegisterPage.tsx index 109eff2..470baa2 100644 --- a/react/src/features/message/pages/MessageRegisterPage.tsx +++ b/react/src/features/message/pages/MessageRegisterPage.tsx @@ -1,16 +1,26 @@ -import { useState, useRef } from "react"; +import { useState, useRef, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { toast } from "sonner"; import PageHeader from "@/components/common/PageHeader"; import useShake from "@/hooks/useShake"; import MessagePreview from "../components/MessagePreview"; -import { SERVICE_OPTIONS, LINK_TYPE } from "../types"; +import { LINK_TYPE } from "../types"; import type { LinkType } from "../types"; +import { fetchServices } from "@/api/service.api"; +import { validateMessage, saveMessage } from "@/api/message.api"; + +interface ServiceOption { + value: string; + label: string; +} export default function MessageRegisterPage() { const navigate = useNavigate(); const { triggerShake, cls } = useShake(); + // 서비스 옵션 (API에서 로드) + const [serviceOptions, setServiceOptions] = useState([]); + // 폼 상태 const [service, setService] = useState(""); const [title, setTitle] = useState(""); @@ -20,8 +30,12 @@ export default function MessageRegisterPage() { const [linkType, setLinkType] = useState(LINK_TYPE.WEB); const [extra, setExtra] = useState(""); + // 서비스 드롭다운 상태 + const [serviceOpen, setServiceOpen] = useState(false); + const serviceDropdownRef = useRef(null); + // 필드 ref (스크롤용) - const serviceRef = useRef(null); + const serviceRef = useRef(null); const titleRef = useRef(null); // 에러 메시지 상태 (shake와 별도로 유지) @@ -29,6 +43,36 @@ export default function MessageRegisterPage() { // 확인 모달 상태 const [showConfirm, setShowConfirm] = useState(false); + const [saving, setSaving] = useState(false); + + // 서비스 드롭다운 외부 클릭 닫기 + useEffect(() => { + function handleClick(e: MouseEvent) { + if ( + serviceDropdownRef.current && + !serviceDropdownRef.current.contains(e.target as Node) + ) { + setServiceOpen(false); + } + } + document.addEventListener("click", handleClick); + return () => document.removeEventListener("click", handleClick); + }, []); + + // 서비스 옵션 로드 + useEffect(() => { + (async () => { + try { + const res = await fetchServices({ page: 1, pageSize: 100 }); + const items = res.data.data.items ?? []; + setServiceOptions( + items.map((s) => ({ value: s.serviceCode, label: s.serviceName })), + ); + } catch { + // 로드 실패 시 빈 배열 유지 + } + })(); + }, []); // 필수 필드 검증 const validate = (): boolean => { @@ -63,10 +107,44 @@ export default function MessageRegisterPage() { }; // 모달 확인 → 저장 실행 - const handleConfirmSave = () => { - setShowConfirm(false); - toast.success("저장이 완료되었습니다."); - setTimeout(() => navigate("/messages"), 600); + const handleConfirmSave = async () => { + setSaving(true); + try { + // 서버 검증 + await validateMessage( + { + title, + body, + image_url: imageUrl || null, + link_url: linkUrl || null, + link_type: linkType || null, + data: extra || undefined, + }, + service, + ); + + // 저장 + await saveMessage( + { + title, + body: body || null, + image_url: imageUrl || null, + link_url: linkUrl || null, + link_type: linkType || null, + data: extra || null, + }, + service, + ); + + setShowConfirm(false); + toast.success("저장이 완료되었습니다."); + setTimeout(() => navigate("/messages"), 600); + } catch { + setShowConfirm(false); + toast.error("저장에 실패했습니다."); + } finally { + setSaving(false); + } }; // 입력 시 해당 필드 에러 제거 @@ -107,33 +185,46 @@ export default function MessageRegisterPage() { -
- - - expand_more - + + {service + ? serviceOptions.find((o) => o.value === service) + ?.label + : "서비스를 선택하세요"} + + + expand_more + + + {serviceOpen && ( +
    + {serviceOptions.map((opt) => ( +
  • { + setService(opt.value); + clearError("service"); + setServiceOpen(false); + }} + className={`px-4 py-2.5 text-sm hover:bg-gray-50 cursor-pointer transition-colors ${ + opt.value === service + ? "text-[#2563EB] font-medium" + : "text-[#0f172a]" + }`} + > + {opt.label} +
  • + ))} +
+ )}
{errors.service && (

@@ -299,7 +390,7 @@ export default function MessageRegisterPage() { title={title} body={body} hasImage={!!imageUrl.trim()} - appName={SERVICE_OPTIONS.find((o) => o.value === service)?.label} + appName={serviceOptions.find((o) => o.value === service)?.label} variant="large" /> @@ -340,18 +431,20 @@ export default function MessageRegisterPage() {

diff --git a/react/src/features/message/types.ts b/react/src/features/message/types.ts index 4aed4ec..da60ba5 100644 --- a/react/src/features/message/types.ts +++ b/react/src/features/message/types.ts @@ -6,133 +6,88 @@ export const LINK_TYPE = { export type LinkType = (typeof LINK_TYPE)[keyof typeof LINK_TYPE]; -// 메시지 목록용 요약 -export interface MessageSummary { - messageId: string; - title: string; - serviceName: string; - createdAt: string; +// ── 목록 ── + +/** 목록 요청 */ +export interface MessageListRequest { + page: number; + size: number; + keyword?: string | null; + is_active?: boolean | null; + service_code?: string | null; + send_status?: string | null; } -// 메시지 상세 -export interface MessageDetail extends MessageSummary { - body: string; - imageUrl: string; - linkUrl: string; - linkType: LinkType; - extra: string; +/** 목록 응답 아이템 */ +export interface MessageListItem { + message_code: string | null; + title: string | null; + service_name: string | null; + service_code: string | null; + send_status: string | null; + created_at: string | null; + is_active: boolean; } -// 메시지 작성 폼 데이터 -export interface MessageFormData { - service: string; +/** 목록 응답 */ +export interface MessageListResponse { + items: MessageListItem[] | null; + totalCount: number; + page: number; + size: number; + totalPages: number; +} + +// ── 상세 ── + +/** 상세 요청 */ +export interface MessageInfoRequest { + message_code: string | null; +} + +/** 상세 응답 */ +export interface MessageInfoResponse { + message_code: string | null; + title: string | null; + body: string | null; + image_url: string | null; + link_url: string | null; + link_type: string | null; + data: unknown; + service_name: string | null; + service_code: string | null; + created_by_name: string | null; + latest_send_status: string | null; + created_at: string | null; +} + +// ── 저장 ── + +/** 저장 요청 */ +export interface MessageSaveRequest { + title: string | null; + body: string | null; + image_url: string | null; + link_url: string | null; + link_type: string | null; + data: unknown; +} + +// ── 삭제 ── + +/** 삭제 요청 */ +export interface MessageDeleteRequest { + message_code: string | null; +} + +// ── 검증 ── + +/** 검증 요청 */ +export interface MessageValidateRequest { title: string; body: string; - imageUrl: string; - linkUrl: string; - linkType: LinkType; - extra: string; + image_url?: string | null; + link_url?: string | null; + link_type?: string | null; + data?: unknown; } - -// 서비스 옵션 (작성 폼 select용) -export const SERVICE_OPTIONS = [ - { value: "spms-shop", label: "SPMS 쇼핑몰" }, - { value: "spms-partner", label: "SPMS 파트너 센터" }, - { value: "spms-delivery", label: "SPMS 딜리버리" }, -] as const; - -// 서비스 필터 옵션 (목록 필터용) -export const SERVICE_FILTER_OPTIONS = [ - "전체 서비스", - "Main App", - "Admin Portal", - "API Gateway", -]; - -// 목 데이터 - 메시지 목록 -export const MOCK_MESSAGES: MessageSummary[] = [ - { messageId: "MSG-005", title: "[공지] 시스템 점검 안내 (2024-05-20)", serviceName: "Main App", createdAt: "2024-05-18" }, - { messageId: "MSG-004", title: "신규 가입 환영 웰컴 메시지", serviceName: "Admin Portal", createdAt: "2024-05-19" }, - { messageId: "MSG-003", title: "API 인증 오류 리포트 알림", serviceName: "API Gateway", createdAt: "2024-05-19" }, - { messageId: "MSG-002", title: "결제 시스템 업데이트 완료 안내", serviceName: "Main App", createdAt: "2024-05-17" }, - { messageId: "MSG-001", title: "[광고] 시즌 프로모션 혜택 안내", serviceName: "Main App", createdAt: "2024-05-16" }, - { messageId: "MSG-006", title: "배송 지연 안내 긴급 알림", serviceName: "Main App", createdAt: "2024-05-15" }, - { messageId: "MSG-007", title: "[이벤트] 출석 체크 보상 안내", serviceName: "Admin Portal", createdAt: "2024-05-14" }, - { messageId: "MSG-008", title: "서버 점검 완료 및 정상화 안내", serviceName: "API Gateway", createdAt: "2024-05-13" }, - { messageId: "MSG-009", title: "개인정보 처리방침 변경 안내", serviceName: "Main App", createdAt: "2024-05-12" }, - { messageId: "MSG-010", title: "[광고] 신규 회원 가입 혜택 안내", serviceName: "Admin Portal", createdAt: "2024-05-11" }, -]; - -// 목 데이터 - 메시지 상세 (messageId → 상세 정보) -export const MOCK_MESSAGE_DETAILS: Record = { - "MSG-001": { - messageId: "MSG-001", title: "[광고] 시즌 프로모션 혜택 안내", serviceName: "Main App", createdAt: "2024-05-16", - body: "안녕하세요! 시즌 프로모션이 시작되었습니다. 지금 바로 참여하고 다양한 혜택을 받아보세요. 최대 50% 할인 쿠폰과 추가 적립 포인트가 제공됩니다.", - imageUrl: "https://cdn.spms.io/promo/summer-sale-banner.jpg", - linkUrl: "https://shop.spms.io/promo/summer2024", linkType: "web", - extra: "프로모션 기간: 2024-05-16 ~ 2024-06-30\n대상: 전체 회원\n쿠폰 코드: SUMMER50", - }, - "MSG-002": { - messageId: "MSG-002", title: "결제 시스템 업데이트 완료 안내", serviceName: "Main App", createdAt: "2024-05-17", - body: "결제 시스템이 최신 버전으로 업데이트되었습니다. 새로운 결제 수단이 추가되었으며, 더 빠르고 안전한 결제 경험을 제공합니다.", - imageUrl: "", - linkUrl: "https://shop.spms.io/notice/payment-update", linkType: "web", - extra: "변경사항: 네이버페이, 카카오페이 간편결제 추가\nPG사: KG이니시스 v2.5 적용", - }, - "MSG-003": { - messageId: "MSG-003", title: "API 인증 오류 리포트 알림", serviceName: "API Gateway", createdAt: "2024-05-19", - body: "API 인증 과정에서 오류가 감지되었습니다. 일부 클라이언트에서 토큰 갱신 실패가 발생하고 있으니 확인 부탁드립니다.", - imageUrl: "", - linkUrl: "spms://admin/api-monitor", linkType: "deeplink", - extra: "오류 코드: AUTH_TOKEN_EXPIRED\n영향 범위: v2.3 이하 클라이언트\n임시 조치: 수동 토큰 재발급 필요", - }, - "MSG-004": { - messageId: "MSG-004", title: "신규 가입 환영 웰컴 메시지", serviceName: "Admin Portal", createdAt: "2024-05-19", - body: "환영합니다! SPMS 서비스에 가입해 주셔서 감사합니다. 신규 회원 혜택으로 7일간 프리미엄 기능을 무료로 이용하실 수 있습니다.", - imageUrl: "https://cdn.spms.io/welcome/onboarding-guide.png", - linkUrl: "spms://onboarding/start", linkType: "deeplink", - extra: "혜택 만료일: 가입일 기준 7일\n자동 전환: Basic 플랜", - }, - "MSG-005": { - messageId: "MSG-005", title: "[공지] 시스템 점검 안내 (2024-05-20)", serviceName: "Main App", createdAt: "2024-05-18", - body: "서비스 안정성 개선을 위해 시스템 점검이 예정되어 있습니다. 점검 시간: 2024-05-20 02:00 ~ 06:00 (약 4시간). 해당 시간에는 서비스 이용이 제한됩니다.", - imageUrl: "", - linkUrl: "https://status.spms.io", linkType: "web", - extra: "점검 유형: 정기 점검\nDB 마이그레이션 포함\n긴급 연락: ops@spms.io", - }, - "MSG-006": { - messageId: "MSG-006", title: "배송 지연 안내 긴급 알림", serviceName: "Main App", createdAt: "2024-05-15", - body: "고객님의 주문 상품 배송이 지연되고 있습니다. 물류 센터 사정으로 인해 1~2일 추가 소요될 수 있습니다. 불편을 드려 죄송합니다.", - imageUrl: "", - linkUrl: "spms://order/tracking", linkType: "deeplink", - extra: "지연 사유: 물류 센터 과부하\n보상: 500 포인트 자동 지급", - }, - "MSG-007": { - messageId: "MSG-007", title: "[이벤트] 출석 체크 보상 안내", serviceName: "Admin Portal", createdAt: "2024-05-14", - body: "출석 체크 이벤트에 참여해 주셔서 감사합니다! 7일 연속 출석 보상으로 500 포인트가 지급되었습니다. 마이페이지에서 확인해 주세요.", - imageUrl: "https://cdn.spms.io/event/attendance-reward.png", - linkUrl: "spms://mypage/point", linkType: "deeplink", - extra: "이벤트 기간: 2024-05-01 ~ 2024-05-31\n추가 보너스: 30일 연속 출석 시 5,000P", - }, - "MSG-008": { - messageId: "MSG-008", title: "서버 점검 완료 및 정상화 안내", serviceName: "API Gateway", createdAt: "2024-05-13", - body: "서버 정기 점검이 완료되었습니다. 모든 서비스가 정상적으로 운영되고 있습니다. 이용에 불편을 드려 죄송합니다.", - imageUrl: "", - linkUrl: "https://status.spms.io", linkType: "web", - extra: "점검 소요 시간: 3시간 42분\n적용 패치: v3.8.2", - }, - "MSG-009": { - messageId: "MSG-009", title: "개인정보 처리방침 변경 안내", serviceName: "Main App", createdAt: "2024-05-12", - body: "개인정보 처리방침이 변경되었습니다. 주요 변경 사항: 수집 항목 변경, 보유 기간 조정. 자세한 내용은 설정 > 개인정보처리방침에서 확인해 주세요.", - imageUrl: "", - linkUrl: "https://shop.spms.io/privacy-policy", linkType: "web", - extra: "시행일: 2024-06-01\n주요 변경: 마케팅 수집 항목 세분화\n법적 근거: 개인정보보호법 제15조", - }, - "MSG-010": { - messageId: "MSG-010", title: "[광고] 신규 회원 가입 혜택 안내", serviceName: "Admin Portal", createdAt: "2024-05-11", - body: "신규 회원 가입 시 다양한 혜택을 제공합니다. 가입 즉시 2,000원 할인 쿠폰과 무료 배송 쿠폰을 드립니다.", - imageUrl: "https://cdn.spms.io/ad/signup-benefit.jpg", - linkUrl: "https://shop.spms.io/signup", linkType: "web", - extra: "실패 사유: 수신 대상 토큰 만료\n재시도 예정: 수동 재발송 필요", - }, -}; -- 2.45.1
@@ -221,9 +271,9 @@ export default function MessageListPage() { onClick={(e) => e.stopPropagation()} > - {msg.messageId} + {msg.message_code} - + - {msg.serviceName} + {msg.service_name} - {formatDate(msg.createdAt)} + {formatDate(msg.created_at ?? "")}