[cs] WIP: 알림톡발송이력관리 페이지

This commit is contained in:
2026-04-13 15:27:31 +09:00
parent 9d18ef7527
commit 43fe3a1a99
16 changed files with 702 additions and 56 deletions
@@ -0,0 +1 @@
export { NtftalkHistoryPage } from "./ui/ntftalk-history-page";
@@ -0,0 +1,64 @@
import type { ColumnDef } from "@tanstack/react-table";
import type { NtftalkMmtLogHistory } from "@/entities/ntftalk";
function formatDateTime(value: string | null | undefined): string {
if (!value) return "—";
const normalized = value.includes("T")
? value.replace("T", " ").slice(0, 19)
: value.slice(0, 19);
return normalized;
}
function formatResult(successYn: string | null | undefined): string {
if (!successYn) return "—";
if (successYn === "SUCCESS") return "성공";
if (successYn === "FAIL") return "실패";
return successYn;
}
export function createNtftalkHistoryColumns(): ColumnDef<NtftalkMmtLogHistory>[] {
return [
{
id: "rowNo",
header: "순번",
enableSorting: false,
cell: ({ row, table }) => {
const { pageIndex, pageSize } = table.getState().pagination;
return pageIndex * pageSize + row.index + 1;
},
},
{
accessorKey: "siteSystem",
header: "시스템",
cell: ({ getValue }) => (getValue() as string) || "—",
},
{
accessorKey: "content",
header: "알림내용",
cell: ({ getValue }) => {
const v = getValue() as string;
if (!v) return "—";
return (
<span className="line-clamp-2 max-w-[28rem] text-left" title={v}>
{v}
</span>
);
},
},
{
accessorKey: "recipientNum",
header: "수신자",
cell: ({ getValue }) => (getValue() as string) || "—",
},
{
accessorKey: "dateRslt",
header: "수신일",
cell: ({ getValue }) => formatDateTime(getValue() as string | null),
},
{
accessorKey: "successYn",
header: "결과",
cell: ({ getValue }) => formatResult(getValue() as string | null),
},
];
}
@@ -0,0 +1,247 @@
"use client";
import { useMemo, useState } from "react";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import dayjs from "dayjs";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
import {
NtftalkMmtLogHistoryApi,
type NtftalkMmtLogHistorySearchParams,
} from "@/entities/ntftalk";
import { SiteSelect } from "@/entities/site";
import {
Button,
Input,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@common/components";
import { createNtftalkHistoryColumns } from "../model/ntftalk-history-table-columns";
type DateRange = {
from?: Date | null;
to?: Date | null;
};
type PresetPeriod = "" | "3M" | "6M" | "1Y" | "CUSTOM";
function computeDateRangeFromPreset(
preset: Extract<PresetPeriod, "3M" | "6M" | "1Y">,
): DateRange {
const to = dayjs().endOf("day").toDate();
const base = dayjs();
let fromDay: dayjs.Dayjs;
switch (preset) {
case "3M":
fromDay = base.subtract(3, "month");
break;
case "6M":
fromDay = base.subtract(6, "month");
break;
case "1Y":
fromDay = base.subtract(1, "year");
break;
}
return { from: fromDay.startOf("day").toDate(), to };
}
function NtftalkHistoryPage() {
const [contentInput, setContentInput] = useState("");
const [recipientInput, setRecipientInput] = useState("");
const [presetPeriod, setPresetPeriod] = useState<PresetPeriod>("");
const [dateRange, setDateRange] = useState<DateRange>({
from: null,
to: null,
});
const [resultFilter, setResultFilter] = useState("ALL");
const [systemFilter, setSystemFilter] = useState("ALL");
const [searchParams, setSearchParams] =
useState<NtftalkMmtLogHistorySearchParams>({ sort: "dateMtReport,desc" });
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const listQuery = useQuery({
queryKey: [
ENDPOINT.NTFTALK_SERVICE.MMT_LOG_HISTORY,
searchParams,
pagination,
],
queryFn: () =>
NtftalkMmtLogHistoryApi.getNtftalkMmtLogHistoryPage({
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
placeholderData: keepPreviousData,
});
const columns = useMemo(() => createNtftalkHistoryColumns(), []);
const { tableComponent } = useDataTable({
query: listQuery,
tableOptions: {
columns,
state: { pagination },
manualPagination: true,
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
},
dataTableOptions: {
viewPagination: true,
},
});
const buildSearchParamsFromInputs = (): NtftalkMmtLogHistorySearchParams => ({
sort: searchParams.sort ?? "dateMtReport,desc",
content: contentInput.trim() || undefined,
recipientNum: recipientInput.trim() || undefined,
successYn: resultFilter,
system: systemFilter,
dateFrom: dateRange.from
? dayjs(dateRange.from).format("YYYY-MM-DD")
: undefined,
dateTo: dateRange.to ? dayjs(dateRange.to).format("YYYY-MM-DD") : undefined,
});
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
setPagination((p) => ({ ...p, pageIndex: 0 }));
setSearchParams(buildSearchParamsFromInputs());
};
const handlePresetPeriodChange = (value: PresetPeriod) => {
setPresetPeriod(value);
if (value === "3M" || value === "6M" || value === "1Y") {
setDateRange(computeDateRangeFromPreset(value));
}
};
const handleDateRangeChange = (next: DateRange) => {
setDateRange(next);
setPresetPeriod("CUSTOM");
};
const handleReset = () => {
setContentInput("");
setRecipientInput("");
setPresetPeriod("");
setDateRange({ from: null, to: null });
setResultFilter("ALL");
setSystemFilter("ALL");
setPagination((p) => ({ ...p, pageIndex: 0 }));
setSearchParams({ sort: "dateMtReport,desc" });
};
return (
<div className="card card-border !mt-6 flex flex-col gap-4">
<div className="card card-border">
<form
onSubmit={handleSearch}
className="flex flex-col gap-4 p-2 md:flex-row md:items-end md:justify-between"
>
<div className="flex flex-col gap-4">
<div className="flex flex-wrap items-end gap-4">
<div className="flex flex-wrap items-center gap-2">
<Label className="whitespace-nowrap text-label">조회기간</Label>
<Select
value={presetPeriod}
onValueChange={handlePresetPeriodChange}
>
<SelectTrigger className="w-[16rem]">
<SelectValue placeholder="기간 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="3M">3개월</SelectItem>
<SelectItem value="6M">6개월</SelectItem>
<SelectItem value="1Y">1년</SelectItem>
<SelectItem value="CUSTOM">직접입력</SelectItem>
</SelectContent>
</Select>
<InputDateRangePicker
dateRange={dateRange}
setDateRange={handleDateRangeChange}
disabled={presetPeriod !== "CUSTOM"}
/>
</div>
</div>
<div className="flex flex-wrap gap-6">
<div className="flex items-center gap-2">
<Label className="whitespace-nowrap text-label">알림내용</Label>
<Input
className="w-[16rem]"
value={contentInput}
onChange={(e) => setContentInput(e.target.value)}
/>
</div>
<div className="flex items-center gap-2">
<Label className="whitespace-nowrap text-label">사용자</Label>
<Input
className="w-[16rem]"
value={recipientInput}
onChange={(e) => setRecipientInput(e.target.value)}
placeholder="수신자 번호"
/>
</div>
<div className="flex items-center gap-2">
<Label className="whitespace-nowrap text-label">결과</Label>
<Select value={resultFilter} onValueChange={setResultFilter}>
<SelectTrigger className="w-[16rem]">
<SelectValue placeholder="전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">전체</SelectItem>
<SelectItem value="SUCCESS">성공</SelectItem>
<SelectItem value="FAIL">실패</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<Label className="whitespace-nowrap text-label">시스템</Label>
<SiteSelect
className="w-[16rem]"
value={systemFilter}
onValueChange={(value) => setSystemFilter(value)}
enabledAllSite
/>
</div>
</div>
</div>
<div className="flex shrink-0 gap-2">
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</div>
</form>
</div>
<div className="card card-border">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">알림톡 발송 이력</h3>
</div>
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
</div>
</div>
);
}
export { NtftalkHistoryPage };
@@ -1,6 +1,10 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import { NoData } from "@/shared/ui/no-data";
import { NtftalkTempApi } from "@/entities/ntftalk";
import {
Dialog,
DialogContent,
@@ -13,9 +17,6 @@ import {
TableHead,
TableRow,
} from "@common/components";
import { NtftalkTempApi } from "@/entities/ntftalk";
import { ENDPOINT } from "@/shared/config";
import { NoData } from "@/shared/ui/no-data";
interface NtftalkTempDetailDialogProps {
templateCode: string | null;
@@ -2,12 +2,38 @@
import { useCallback, useMemo, useState } from "react";
import { useFieldArray, useForm, useWatch } from "react-hook-form";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
keepPreviousData,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import {
getCoreRowModel,
type PaginationState,
type RowSelectionState,
useReactTable,
} from "@tanstack/react-table";
import { z } from "zod";
import { AUTH_USER_ID, ENDPOINT } from "@/shared/config";
import {
getCookie,
useAlertStore,
useDataTable,
useMutationWrapper,
} from "@/shared/lib";
import { NoData } from "@/shared/ui/no-data";
import {
NtftalkBiztalkApi,
type NtftalkBiztalkInspectionRequestItem,
type NtftalkTemp,
NtftalkTempApi,
type NtftalkTempSearchParams,
NtftalkVarApi,
type NtftalkVarSearchParams,
} from "@/entities/ntftalk";
import {
Button,
DataTable,
@@ -20,36 +46,14 @@ import {
SelectTrigger,
SelectValue,
} from "@common/components";
import {
getCoreRowModel,
type PaginationState,
type RowSelectionState,
useReactTable,
} from "@tanstack/react-table";
import { z } from "zod";
import {
NtftalkBiztalkApi,
NtftalkTempApi,
NtftalkVarApi,
type NtftalkBiztalkInspectionRequestItem,
type NtftalkTemp,
type NtftalkTempSearchParams,
type NtftalkVarSearchParams,
} from "@/entities/ntftalk";
import { AUTH_USER_ID, ENDPOINT } from "@/shared/config";
import {
getCookie,
useAlertStore,
useDataTable,
useMutationWrapper,
} from "@/shared/lib";
import { NoData } from "@/shared/ui/no-data";
import { createTemplateColumns } from "../model/template-table-columns";
import {
createEmptyVarDraft,
type NtftalkVarGridRow,
} from "../model/ntftalk-var-grid";
import { createTemplateColumns } from "../model/template-table-columns";
import { createVarColumns } from "../model/var-table-columns";
import { NtftalkTempDetailDialog } from "./ntftalk-temp-detail-dialog";
import { NtftalkTempRegistDialog } from "./ntftalk-temp-regist-dialog";
@@ -1,8 +1,9 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
keepPreviousData,
useQuery,
@@ -13,10 +14,28 @@ import {
type PaginationState,
type RowSelectionState,
} from "@tanstack/react-table";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/shared/ui/form";
import { NoData } from "@/shared/ui/no-data";
import {
NtftalkBiztalkApi,
NtftalkTempApi,
NtftalkVarApi,
type NtftalkVarSearchParams,
} from "@/entities/ntftalk";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Input,
@@ -33,29 +52,13 @@ import {
TableBody,
TableCell,
TableHead,
Textarea,
TableRow,
DialogFooter,
Textarea,
} from "@common/components";
import {
NtftalkBiztalkApi,
NtftalkTempApi,
NtftalkVarApi,
type NtftalkVarSearchParams,
} from "@/entities/ntftalk";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
import { NoData } from "@/shared/ui/no-data";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/shared/ui/form";
import {
ntftalkTempSaveFormSchema,
type NtftalkTempSaveFormData,
ntftalkTempSaveFormSchema,
} from "../model/ntftalk-temp-save-schema";
import { createVarColumns } from "../model/var-table-columns";