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 */}
@@ -221,9 +271,9 @@ export default function MessageListPage() {
onClick={(e) => e.stopPropagation()}
>
- {msg.messageId}
+ {msg.message_code}
-
+
|
{/* 메시지 제목 */}
@@ -235,13 +285,13 @@ export default function MessageListPage() {
{/* 서비스 */}
- {msg.serviceName}
+ {msg.service_name}
|
{/* 작성일 */}
- {formatDate(msg.createdAt)}
+ {formatDate(msg.created_at ?? "")}
|
@@ -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재시도 예정: 수동 재발송 필요",
- },
-};