813 lines
36 KiB
TypeScript
813 lines
36 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo, useState } from "react";
|
|
|
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
|
import {
|
|
getCoreRowModel,
|
|
type RowSelectionState,
|
|
useReactTable,
|
|
} from "@tanstack/react-table";
|
|
import type { AxiosError } from "axios";
|
|
|
|
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
|
import type { ApiErrorResponse } from "@/shared/model";
|
|
import {
|
|
type AdstZoneResponseDto,
|
|
type BusinessSendRequest,
|
|
type BusinessSendResponse,
|
|
noticeTalkSendingApi,
|
|
type SelectedRecipient,
|
|
type SewerStUserItem,
|
|
useNtftalkTemplateList,
|
|
useNtftalkTemplateVariables,
|
|
userListColumns,
|
|
useSewerStUserList,
|
|
useStAnlsSidoCombo,
|
|
useStAnlsSigunguCombo,
|
|
} from "@/entities/statistics";
|
|
import {
|
|
Button,
|
|
DataTable,
|
|
Dialog,
|
|
DialogClose,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
Input,
|
|
Label,
|
|
LoadingWithAnimation,
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@common/components";
|
|
|
|
type CommonVariableKey =
|
|
| "userNm"
|
|
| "sido"
|
|
| "gugun"
|
|
| "gubunNm"
|
|
| "deptNm"
|
|
| "jbgdNm"
|
|
| "telNo"
|
|
| "custom";
|
|
|
|
const EMPTY_ADST_LIST: AdstZoneResponseDto[] = [];
|
|
const GUBUN_OPTIONS = [
|
|
{ value: "PIC", label: "입력자" },
|
|
{ value: "IDFR", label: "승인자" },
|
|
{ value: "MCTPV", label: "검토자" },
|
|
] as const;
|
|
|
|
function getDefaultStatsYear(): string {
|
|
return String(new Date().getFullYear() - 1);
|
|
}
|
|
|
|
function buildTemplateMessage(
|
|
templateContent: string,
|
|
variables: Record<string, string>,
|
|
): string {
|
|
return templateContent.replace(/#?\{([^}]+)\}/g, (_, key: string) => {
|
|
const value = variables[key.trim()];
|
|
return value ?? "";
|
|
});
|
|
}
|
|
|
|
function getMergedVariables(
|
|
recipient: SelectedRecipient,
|
|
commonValues: Record<string, string>,
|
|
): Record<string, string> {
|
|
return {
|
|
...commonValues,
|
|
...recipient.variableOverrides,
|
|
TEL_NUM: recipient.telNo,
|
|
};
|
|
}
|
|
|
|
function NtitlkSndng() {
|
|
const alert = useAlertStore((state) => state.setMessage);
|
|
const { confirm } = useConfirm();
|
|
|
|
const [searchStatsYr, setSearchStatsYr] = useState(getDefaultStatsYear());
|
|
const [searchSido, setSearchSido] = useState("");
|
|
const [searchSigungu, setSearchSigungu] = useState("");
|
|
const [searchGubun, setSearchGubun] = useState("");
|
|
|
|
const [appliedParams, setAppliedParams] = useState({
|
|
statsYr: getDefaultStatsYear(),
|
|
sewerBzmnCd: "",
|
|
gubun: "",
|
|
});
|
|
|
|
const [selectedTemplateCode, setSelectedTemplateCode] = useState("");
|
|
const [selectedRecipients, setSelectedRecipients] = useState<
|
|
SelectedRecipient[]
|
|
>([]);
|
|
const [commonVariableValues, setCommonVariableValues] = useState<
|
|
Record<string, string>
|
|
>({});
|
|
const [commonVariableMapping, setCommonVariableMapping] = useState<
|
|
Record<string, CommonVariableKey>
|
|
>({});
|
|
|
|
const [previewRecipient, setPreviewRecipient] =
|
|
useState<SelectedRecipient | null>(null);
|
|
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
|
const [userListRowSelection, setUserListRowSelection] =
|
|
useState<RowSelectionState>({});
|
|
|
|
const sendMutation = useMutationWrapper<
|
|
BusinessSendResponse,
|
|
AxiosError<ApiErrorResponse>,
|
|
BusinessSendRequest
|
|
>({
|
|
mutationFn: (payload) => noticeTalkSendingApi.sendBusiness(payload),
|
|
onSuccess: (data) => {
|
|
if (!data.success) {
|
|
alert(data.message || "알림톡 발송 요청에 실패했습니다.");
|
|
return;
|
|
}
|
|
alert(data.message || "알림톡 발송이 요청되었습니다.");
|
|
setSelectedRecipients([]);
|
|
setCommonVariableValues({});
|
|
},
|
|
});
|
|
|
|
const { data: templatePageData } = useNtftalkTemplateList();
|
|
const templates = templatePageData?.content ?? [];
|
|
|
|
const { data: templateVariablesData } =
|
|
useNtftalkTemplateVariables(selectedTemplateCode);
|
|
const variableNames = templateVariablesData?.variables ?? [];
|
|
|
|
const { data: sidoData } = useStAnlsSidoCombo();
|
|
const { data: sigunguData } = useStAnlsSigunguCombo(searchSido);
|
|
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
|
|
const sigunguOptions = sigunguData ?? EMPTY_ADST_LIST;
|
|
|
|
const { data: userListResponse, isLoading: isUserListLoading } =
|
|
useSewerStUserList(appliedParams);
|
|
const userList = (userListResponse as unknown as SewerStUserItem[]) ?? [];
|
|
const userListTable = useReactTable({
|
|
data: userList,
|
|
columns: userListColumns,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
enableRowSelection: true,
|
|
enableMultiRowSelection: true,
|
|
state: {
|
|
rowSelection: userListRowSelection,
|
|
},
|
|
onRowSelectionChange: setUserListRowSelection,
|
|
getRowId: (row, i) => `${i}-${row.telNo}`,
|
|
});
|
|
|
|
const selectedTemplate = useMemo(
|
|
() =>
|
|
templates.find(
|
|
(template) => template.templateCode === selectedTemplateCode,
|
|
),
|
|
[templates, selectedTemplateCode],
|
|
);
|
|
|
|
const previewMessage = useMemo(() => {
|
|
if (!previewRecipient || !selectedTemplate?.templateContent) return "";
|
|
return buildTemplateMessage(
|
|
selectedTemplate.templateContent,
|
|
getMergedVariables(previewRecipient, commonVariableValues),
|
|
);
|
|
}, [previewRecipient, selectedTemplate, commonVariableValues]);
|
|
|
|
const handleTemplateChange = (templateCode: string) => {
|
|
setSelectedTemplateCode(templateCode);
|
|
setCommonVariableValues({});
|
|
setCommonVariableMapping({});
|
|
setSelectedRecipients((prev) =>
|
|
prev.map((recipient) => ({ ...recipient, variableOverrides: {} })),
|
|
);
|
|
};
|
|
|
|
const handleSearch = () => {
|
|
const sewerBzmnCd =
|
|
searchSido !== ""
|
|
? searchSigungu !== ""
|
|
? searchSigungu
|
|
: searchSido
|
|
: "";
|
|
|
|
setAppliedParams({
|
|
statsYr: searchStatsYr,
|
|
sewerBzmnCd,
|
|
gubun: searchGubun,
|
|
});
|
|
setUserListRowSelection({});
|
|
};
|
|
|
|
const handleReset = () => {
|
|
const defaultYear = getDefaultStatsYear();
|
|
setSearchStatsYr(defaultYear);
|
|
setSearchSido("");
|
|
setSearchSigungu("");
|
|
setSearchGubun("");
|
|
setAppliedParams({
|
|
statsYr: defaultYear,
|
|
sewerBzmnCd: "",
|
|
gubun: "",
|
|
});
|
|
setUserListRowSelection({});
|
|
};
|
|
|
|
const handleRemoveRecipient = (telNo: string) => {
|
|
setSelectedRecipients((prev) =>
|
|
prev.filter((recipient) => recipient.telNo !== telNo),
|
|
);
|
|
};
|
|
|
|
const handleAddSelectedToRecipients = () => {
|
|
const rows = userListTable.getSelectedRowModel().rows;
|
|
if (rows.length === 0) {
|
|
alert("추가할 담당자를 선택해주세요.");
|
|
return;
|
|
}
|
|
|
|
setSelectedRecipients((prev) => {
|
|
const existingTelNos = new Set(prev.map((r) => r.telNo));
|
|
const newRecipients = rows
|
|
.map((row) => row.original)
|
|
.filter((recipient) => !existingTelNos.has(recipient.telNo))
|
|
.map((recipient) => ({
|
|
...recipient,
|
|
variableOverrides: {},
|
|
}));
|
|
|
|
return [...prev, ...newRecipients];
|
|
});
|
|
|
|
setUserListRowSelection({});
|
|
};
|
|
|
|
const handleApplyCommonValues = () => {
|
|
setSelectedRecipients((prev) =>
|
|
prev.map((recipient) => {
|
|
const newOverrides: Record<string, string> = {};
|
|
|
|
variableNames.forEach((variableName) => {
|
|
const mappingType = commonVariableMapping[variableName];
|
|
|
|
if (!mappingType) return;
|
|
|
|
if (mappingType === "custom") {
|
|
newOverrides[variableName] =
|
|
commonVariableValues[variableName] ?? "";
|
|
} else {
|
|
newOverrides[variableName] =
|
|
(recipient[
|
|
mappingType as keyof SewerStUserItem
|
|
] as string) ?? "";
|
|
}
|
|
});
|
|
|
|
return {
|
|
...recipient,
|
|
variableOverrides: {
|
|
...recipient.variableOverrides,
|
|
...newOverrides,
|
|
},
|
|
};
|
|
}),
|
|
);
|
|
setCommonVariableMapping({});
|
|
};
|
|
|
|
const handleResetCommonValues = () => {
|
|
setCommonVariableMapping({});
|
|
setCommonVariableValues({});
|
|
};
|
|
|
|
const handleRecipientVariableChange = (
|
|
telNo: string,
|
|
variableName: string,
|
|
value: string,
|
|
) => {
|
|
setSelectedRecipients((prev) =>
|
|
prev.map((recipient) =>
|
|
recipient.telNo === telNo
|
|
? {
|
|
...recipient,
|
|
variableOverrides: {
|
|
...recipient.variableOverrides,
|
|
[variableName]: value,
|
|
},
|
|
}
|
|
: recipient,
|
|
),
|
|
);
|
|
};
|
|
|
|
const handlePreview = (recipient: SelectedRecipient) => {
|
|
if (!selectedTemplate) {
|
|
alert("알림톡 템플릿을 먼저 선택해주세요.");
|
|
return;
|
|
}
|
|
setPreviewRecipient(recipient);
|
|
setIsPreviewOpen(true);
|
|
};
|
|
|
|
const handleSend = async () => {
|
|
if (!selectedTemplateCode) {
|
|
alert("알림톡 템플릿을 선택해주세요.");
|
|
return;
|
|
}
|
|
|
|
const missingVariable = selectedRecipients.some((recipient) =>
|
|
variableNames.some((name) => {
|
|
const mergedValue =
|
|
recipient.variableOverrides[name] ??
|
|
commonVariableValues[name] ??
|
|
"";
|
|
return mergedValue.trim() === "";
|
|
}),
|
|
);
|
|
if (missingVariable) {
|
|
alert("변수값이 입력되지 않은 발송 대상이 있습니다.");
|
|
return;
|
|
}
|
|
if (
|
|
!(await confirm(
|
|
`${selectedRecipients.length}명에게 발송하시겠습니까?`,
|
|
))
|
|
)
|
|
return;
|
|
|
|
sendMutation.mutate({
|
|
templateCode: selectedTemplateCode,
|
|
callback: "032-590-4000",
|
|
recipients: selectedRecipients.map((recipient) =>
|
|
getMergedVariables(recipient, commonVariableValues),
|
|
),
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-[32rem_minmax(0,1fr)]">
|
|
<section className="card card-border space-y-6 p-4 pb-0">
|
|
<div className="space-y-2">
|
|
<h3 className="text-xl font-semibold">
|
|
알림톡 템플릿 선택
|
|
</h3>
|
|
<div className="flex items-center gap-3">
|
|
<Select
|
|
value={selectedTemplateCode}
|
|
onValueChange={handleTemplateChange}
|
|
>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue placeholder="템플릿 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{templates.map((template) => (
|
|
<SelectItem
|
|
key={template.templateCode}
|
|
value={template.templateCode}
|
|
>
|
|
{template.templateName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<p className="text-lg font-medium">미리보기</p>
|
|
<div className="relative">
|
|
<div className="h-[32rem] overflow-auto rounded-[20px] rounded-b-none bg-[rgb(176,192,207)] p-8 pb-0 before:absolute before:left-0 before:top-0 before:block before:h-full before:w-full before:rounded-[20px] before:rounded-b-none before:border-[10px] before:border-b-0 before:border-black before:content-['']">
|
|
<div className="w-[calc(100%-2rem)] pb-5">
|
|
<span className="text-[1.1rem] text-black">
|
|
하수도통계시스템
|
|
</span>
|
|
<div className="mt-2 rounded-t-[12px] bg-[rgb(247,228,54)] p-4 text-[1.2rem] font-medium text-black">
|
|
알림톡 도착
|
|
</div>
|
|
<div className="whitespace-pre-wrap rounded-b-[12px] bg-white p-4 text-[1.2rem] leading-normal tracking-tight text-black">
|
|
{selectedTemplate?.templateContent ??
|
|
"알림톡 템플릿을 선택하면 미리보기 메시지가 표시됩니다."}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="card card-border p-4">
|
|
<h3 className="text-xl font-semibold">통계 담당자 목록</h3>
|
|
<div className="card card-border mx-0 flex items-center p-4">
|
|
<div className="flex items-center gap-6">
|
|
<div className="flex items-center gap-2">
|
|
<Label className="whitespace-nowrap">
|
|
시도
|
|
</Label>
|
|
<Select
|
|
value={searchSido}
|
|
onValueChange={(value) => {
|
|
setSearchSido(value);
|
|
setSearchSigungu("");
|
|
}}
|
|
>
|
|
<SelectTrigger className="w-[16rem]">
|
|
<SelectValue placeholder="전체" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{sidoOptions.map((option) => (
|
|
<SelectItem
|
|
key={option.adstCd}
|
|
value={option.adstCd}
|
|
>
|
|
{option.adstNm}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Label className="whitespace-nowrap">
|
|
시군구
|
|
</Label>
|
|
<Select
|
|
value={searchSigungu}
|
|
onValueChange={setSearchSigungu}
|
|
disabled={!searchSido}
|
|
>
|
|
<SelectTrigger className="w-[16rem]">
|
|
<SelectValue placeholder="전체" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{sigunguOptions.map((option) => (
|
|
<SelectItem
|
|
key={option.adstCd}
|
|
value={option.adstCd}
|
|
>
|
|
{option.adstNm}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Label className="whitespace-nowrap">
|
|
구분
|
|
</Label>
|
|
<Select
|
|
value={searchGubun}
|
|
onValueChange={setSearchGubun}
|
|
>
|
|
<SelectTrigger className="w-[16rem]">
|
|
<SelectValue placeholder="전체" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{GUBUN_OPTIONS.map((option) => (
|
|
<SelectItem
|
|
key={option.value || "ALL"}
|
|
value={option.value}
|
|
>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<div className="flex w-full justify-end gap-2">
|
|
<Button
|
|
color="white"
|
|
type="button"
|
|
onClick={handleReset}
|
|
>
|
|
초기화
|
|
</Button>
|
|
<Button type="button" onClick={handleSearch}>
|
|
검색
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="h-[28rem] overflow-x-auto rounded-md border">
|
|
{isUserListLoading ? (
|
|
<div className="flex items-center justify-center p-6">
|
|
<div className="relative flex size-[6rem] justify-center">
|
|
<LoadingWithAnimation color="primary" />
|
|
</div>
|
|
</div>
|
|
) : userList.length === 0 ? (
|
|
<div className="p-6 text-center text-muted-foreground">
|
|
조회된 담당자가 없습니다.
|
|
</div>
|
|
) : (
|
|
<DataTable
|
|
table={userListTable}
|
|
className="min-w-[72rem] text-sm"
|
|
/>
|
|
)}
|
|
</div>
|
|
{!isUserListLoading && userList.length > 0 && (
|
|
<div className="mt-4 flex items-center justify-end gap-4">
|
|
<p className="text-xl text-muted-foreground">
|
|
선택{" "}
|
|
<span className="font-semibold text-foreground">
|
|
{
|
|
userListTable.getSelectedRowModel().rows
|
|
.length
|
|
}
|
|
명
|
|
</span>{" "}
|
|
/ 전체{" "}
|
|
<span className="font-semibold text-foreground">
|
|
{userList.length}명
|
|
</span>
|
|
</p>
|
|
<Button
|
|
type="button"
|
|
color="gray"
|
|
onClick={handleAddSelectedToRecipients}
|
|
>
|
|
+ 발송 목록 추가
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</section>
|
|
</div>
|
|
|
|
<section className="card card-border p-4">
|
|
<div className="mb-3 flex items-center justify-between">
|
|
<h3 className="text-xl font-semibold">발송 목록</h3>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
type="button"
|
|
color="white"
|
|
onClick={() => setSelectedRecipients([])}
|
|
disabled={selectedRecipients.length === 0}
|
|
>
|
|
초기화
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
color="green"
|
|
onClick={handleSend}
|
|
disabled={
|
|
sendMutation.isPending ||
|
|
selectedRecipients.length === 0
|
|
}
|
|
>
|
|
발송
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{variableNames.length > 0 && (
|
|
<div className="mb-4 space-y-4 rounded-md border p-3">
|
|
<p className="text-lg font-medium">변수 일괄 적용</p>
|
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-6">
|
|
{variableNames.map((variableName) => (
|
|
<div key={variableName} className="space-y-2">
|
|
<Label className="whitespace-nowrap">{`#{${variableName}}`}</Label>
|
|
<Select
|
|
value={
|
|
commonVariableMapping[
|
|
variableName
|
|
] ?? ""
|
|
}
|
|
onValueChange={(value) =>
|
|
setCommonVariableMapping(
|
|
(prev) => ({
|
|
...prev,
|
|
[variableName]:
|
|
value as CommonVariableKey,
|
|
}),
|
|
)
|
|
}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="선택하세요" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="userNm">
|
|
이름
|
|
</SelectItem>
|
|
<SelectItem value="sido">
|
|
시도
|
|
</SelectItem>
|
|
<SelectItem value="gugun">
|
|
시군구
|
|
</SelectItem>
|
|
<SelectItem value="gubunNm">
|
|
구분
|
|
</SelectItem>
|
|
<SelectItem value="deptNm">
|
|
소속
|
|
</SelectItem>
|
|
<SelectItem value="jbgdNm">
|
|
직급
|
|
</SelectItem>
|
|
<SelectItem value="telNo">
|
|
전화번호
|
|
</SelectItem>
|
|
<SelectItem value="custom">
|
|
직접입력
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
{commonVariableMapping[variableName] ===
|
|
"custom" && (
|
|
<Input
|
|
placeholder="값을 입력하세요"
|
|
value={
|
|
commonVariableValues[
|
|
variableName
|
|
] ?? ""
|
|
}
|
|
onChange={(event) =>
|
|
setCommonVariableValues(
|
|
(prev) => ({
|
|
...prev,
|
|
[variableName]:
|
|
event.target.value,
|
|
}),
|
|
)
|
|
}
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
<div className="flex items-end gap-2">
|
|
<Button
|
|
type="button"
|
|
color="white"
|
|
onClick={handleResetCommonValues}
|
|
>
|
|
초기화
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
onClick={handleApplyCommonValues}
|
|
>
|
|
적용하기
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-4">
|
|
{selectedRecipients.length === 0 ? (
|
|
<p className="rounded-md border p-6 text-center text-muted-foreground">
|
|
선택된 발송 대상이 없습니다.
|
|
</p>
|
|
) : (
|
|
<div className="max-h-[40rem] overflow-x-auto rounded-md border">
|
|
<Table variant="line" className="text-sm">
|
|
<TableHeader className="top-0">
|
|
<TableRow>
|
|
<TableHead className="min-w-[100px]">
|
|
이름
|
|
</TableHead>
|
|
<TableHead className="min-w-[120px]">
|
|
전화번호
|
|
</TableHead>
|
|
{variableNames.map((variableName) => (
|
|
<TableHead
|
|
key={variableName}
|
|
className="min-w-[180px]"
|
|
>
|
|
#{variableName}
|
|
</TableHead>
|
|
))}
|
|
<TableHead className="min-w-[160px] text-center">
|
|
버튼
|
|
</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{selectedRecipients.map((recipient, i) => (
|
|
<TableRow
|
|
key={`${i}-${recipient.telNo}`}
|
|
>
|
|
<TableCell align="center">
|
|
{recipient.userNm}
|
|
</TableCell>
|
|
<TableCell align="center">
|
|
{recipient.telNo}
|
|
</TableCell>
|
|
{variableNames.map(
|
|
(variableName) => (
|
|
<TableCell
|
|
key={`${recipient.telNo}-${variableName}`}
|
|
>
|
|
<Input
|
|
value={
|
|
recipient
|
|
.variableOverrides[
|
|
variableName
|
|
] ?? ""
|
|
}
|
|
onChange={(event) =>
|
|
handleRecipientVariableChange(
|
|
recipient.telNo,
|
|
variableName,
|
|
event.target
|
|
.value,
|
|
)
|
|
}
|
|
className="w-full min-w-[150px]"
|
|
/>
|
|
</TableCell>
|
|
),
|
|
)}
|
|
<TableCell align="center">
|
|
<div className="flex justify-center gap-2">
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
color="gray"
|
|
onClick={() =>
|
|
handlePreview(
|
|
recipient,
|
|
)
|
|
}
|
|
>
|
|
미리보기
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
color="red"
|
|
onClick={() =>
|
|
handleRemoveRecipient(
|
|
recipient.telNo,
|
|
)
|
|
}
|
|
>
|
|
제거
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
|
|
<Dialog open={isPreviewOpen} onOpenChange={setIsPreviewOpen}>
|
|
<DialogContent
|
|
className="max-w-[36rem]"
|
|
aria-describedby={undefined}
|
|
>
|
|
<DialogHeader>
|
|
<DialogTitle>알림톡 미리보기</DialogTitle>
|
|
<DialogDescription className="text-[1.1rem] tracking-tight">
|
|
수신자: {previewRecipient?.userNm ?? "-"} /{" "}
|
|
{previewRecipient?.telNo ?? "-"}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="relative">
|
|
<div className="h-[32rem] overflow-auto rounded-[20px] rounded-b-none bg-[rgb(176,192,207)] p-8 pb-0 before:absolute before:left-0 before:top-0 before:block before:h-full before:w-full before:rounded-[20px] before:rounded-b-none before:border-[10px] before:border-b-0 before:border-black before:content-['']">
|
|
<div className="w-[calc(100%-2rem)] pb-5">
|
|
<span className="text-[1.1rem] text-black">
|
|
하수도통계시스템
|
|
</span>
|
|
<div className="mt-2 rounded-t-[12px] bg-[rgb(247,228,54)] p-4 text-[1.2rem] font-medium text-black">
|
|
알림톡 도착
|
|
</div>
|
|
<div className="whitespace-pre-wrap rounded-b-[12px] bg-white p-4 text-[1.2rem] leading-normal tracking-tight text-black">
|
|
{previewMessage ||
|
|
"메시지를 생성할 수 없습니다."}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<DialogClose asChild>
|
|
<Button type="button" color="white">
|
|
닫기
|
|
</Button>
|
|
</DialogClose>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default NtitlkSndng;
|