From e9f707b10b81d9759243473dd03fa463573d0772 Mon Sep 17 00:00:00 2001 From: 0-Zz-ang Date: Fri, 26 Sep 2025 16:45:59 +0900 Subject: (박서영)이메일발신인조회페이지 생성 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/email-log/service.ts | 68 +++++++++++++++++++++++++ lib/email-log/table/email-log-table-columns.tsx | 61 ++++++++++++++++++++++ lib/email-log/table/email-log-table.tsx | 65 +++++++++++++++++++++++ lib/email-log/validations.ts | 18 +++++++ lib/mail/email-log.ts | 24 +++++++++ lib/mail/sendEmail.ts | 9 ++++ 6 files changed, 245 insertions(+) create mode 100644 lib/email-log/service.ts create mode 100644 lib/email-log/table/email-log-table-columns.tsx create mode 100644 lib/email-log/table/email-log-table.tsx create mode 100644 lib/email-log/validations.ts create mode 100644 lib/mail/email-log.ts (limited to 'lib') diff --git a/lib/email-log/service.ts b/lib/email-log/service.ts new file mode 100644 index 00000000..7eea6869 --- /dev/null +++ b/lib/email-log/service.ts @@ -0,0 +1,68 @@ +"use server"; + +import db from "@/db/db"; +import { emailLogs } from "@/db/schema/emailLogs"; +import { and, asc, count, desc, ilike, or } from "drizzle-orm"; +import { type GetEmailLogSchema } from "./validations"; +import { getValidFilters } from "@/lib/data-table"; + +export async function getEmailLogList(input: GetEmailLogSchema) { + const offset = (input.page - 1) * input.perPage; + + const advancedWhere = getValidFilters(input.filters) // placeholder for future advanced filter handling + + let globalWhere; + if (input.search) { + const s = `%${input.search}%`; + globalWhere = or( + ilike(emailLogs.from, s), + ilike(emailLogs.to, s), + ilike(emailLogs.cc, s), + ilike(emailLogs.subject, s), + ); + } + + const conditions = [] as any[]; + if (advancedWhere && (advancedWhere as any).length !== 0) conditions.push(advancedWhere); + if (globalWhere) conditions.push(globalWhere); + + let where: any; + if (conditions.length > 0) { + where = conditions.length > 1 ? and(...conditions) : conditions[0]; + } + + let orderBy; + try { + orderBy = input.sort.length > 0 + ? input.sort + .map((item) => { + if (!item || !item.id || typeof item.id !== "string" || !(item.id in emailLogs)) return null; + const col = emailLogs[item.id as keyof typeof emailLogs]; + return item.desc ? desc(col as any) : asc(col as any); + }) + .filter((v): v is Exclude => v !== null) + : [desc(emailLogs.createdAt)]; + } catch { + orderBy = [desc(emailLogs.createdAt)]; + } + + const data = await db + .select() + .from(emailLogs) + .where(where) + .orderBy(...orderBy as any) + .limit(input.perPage) + .offset(offset); + + const totalResult = await db + .select({ count: count() }) + .from(emailLogs) + .where(where); + + const total = totalResult[0]?.count ?? 0; + const pageCount = Math.ceil(total / input.perPage); + + return { data, pageCount }; +} + + diff --git a/lib/email-log/table/email-log-table-columns.tsx b/lib/email-log/table/email-log-table-columns.tsx new file mode 100644 index 00000000..68f2795f --- /dev/null +++ b/lib/email-log/table/email-log-table-columns.tsx @@ -0,0 +1,61 @@ +"use client" + +import * as React from "react" +import { type ColumnDef } from "@tanstack/react-table" +import { type InferSelectModel } from "drizzle-orm" +import { emailLogs } from "@/db/schema/emailLogs" + +export function getColumns>(): ColumnDef[] { + return [ + { + id: "subject", + header: () =>
Subject
, + accessorKey: "subject", + cell: ({ row }) => ( +
{String(row.original.subject ?? "")}
+ ), + size: 360, + }, + { + id: "from", + header: () =>
From
, + accessorKey: "from", + cell: ({ row }) => ( +
{String(row.original.from ?? "")}
+ ), + size: 220, + }, + { + id: "to", + header: () =>
To
, + accessorKey: "to", + cell: ({ row }) => ( +
{String(row.original.to ?? "")}
+ ), + size: 220, + }, + { + id: "cc", + header: () =>
CC
, + accessorKey: "cc", + cell: ({ row }) => ( +
{String(row.original.cc ?? "")}
+ ), + size: 220, + }, + { + id: "createdAt", + header: () =>
Created At
, + accessorKey: "createdAt", + cell: ({ row }) => ( +
+ {new Date(row.original.createdAt as unknown as string).toLocaleString()} +
+ ), + size: 180, + }, + + ] +} + + diff --git a/lib/email-log/table/email-log-table.tsx b/lib/email-log/table/email-log-table.tsx new file mode 100644 index 00000000..db627af5 --- /dev/null +++ b/lib/email-log/table/email-log-table.tsx @@ -0,0 +1,65 @@ +"use client" + +import * as React from "react" +import type { + DataTableAdvancedFilterField, + DataTableFilterField, +} from "@/types/table" + +import { useDataTable } from "@/hooks/use-data-table" +import { DataTable } from "@/components/data-table/data-table" +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" +import { getColumns } from "./email-log-table-columns" +import { getEmailLogList } from "../service" +import { type InferSelectModel } from "drizzle-orm" +import { emailLogs } from "@/db/schema/emailLogs" + +interface EmailLogTableProps { + promises: Promise<[ + Awaited>, + ]> +} + +export function EmailLogTable({ promises }: EmailLogTableProps) { + const [{ data, pageCount }] = React.use(promises) + + type EmailLog = InferSelectModel + + const columns = React.useMemo(() => getColumns(), []) + + const filterFields: DataTableFilterField[] = [] + + const advancedFilterFields: DataTableAdvancedFilterField[] = [ + { id: "from", label: "From", type: "text" }, + { id: "to", label: "To", type: "text" }, + { id: "subject", label: "Subject", type: "text" }, + { id: "createdAt", label: "발송시간", type: "date" }, + ] + + const { table } = useDataTable({ + data, + columns, + pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "createdAt", desc: true }], + }, + getRowId: (row) => String(row.id), + shallow: false, + clearOnDefault: true, + }) + + return ( + + + + ) +} + + diff --git a/lib/email-log/validations.ts b/lib/email-log/validations.ts new file mode 100644 index 00000000..5554a0aa --- /dev/null +++ b/lib/email-log/validations.ts @@ -0,0 +1,18 @@ +import { createSearchParamsCache, parseAsArrayOf, parseAsInteger, parseAsString, parseAsStringEnum } from "nuqs/server"; +import * as z from "zod"; +import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers"; +import { emailLogs } from "@/db/schema/emailLogs"; + +export const SearchParamsEmailLogCache = createSearchParamsCache({ + flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])) .withDefault([]), + page: parseAsInteger.withDefault(1), + perPage: parseAsInteger.withDefault(10), + sort: getSortingStateParser().withDefault([{ id: "createdAt", desc: true }]), + filters: getFiltersStateParser().withDefault([]), + joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), + search: parseAsString.withDefault(""), +}); + +export type GetEmailLogSchema = Awaited>; + + diff --git a/lib/mail/email-log.ts b/lib/mail/email-log.ts new file mode 100644 index 00000000..bb11aed9 --- /dev/null +++ b/lib/mail/email-log.ts @@ -0,0 +1,24 @@ +import db from "@/db/db"; +import { emailLogs } from "@/db/schema/emailLogs"; + +export type CreateEmailLogParams = { + from: string; + to: string; + cc?: string | string[]; + subject: string; +}; + +export async function createEmailLog(params: CreateEmailLogParams): Promise { + const { from, to, cc, subject } = params; + + const ccValue = Array.isArray(cc) ? cc.join(", ") : cc ?? null; + + await db.insert(emailLogs).values({ + from, + to, + cc: ccValue ?? undefined, + subject, + }); +} + + diff --git a/lib/mail/sendEmail.ts b/lib/mail/sendEmail.ts index 408f6e40..3a4d2591 100644 --- a/lib/mail/sendEmail.ts +++ b/lib/mail/sendEmail.ts @@ -1,5 +1,6 @@ import { useTranslation } from '@/i18n'; import { transporter } from './mailer'; +import { createEmailLog } from '@/lib/mail/email-log'; import db from '@/db/db'; import { templateDetailView } from '@/db/schema'; import { eq } from 'drizzle-orm'; @@ -95,6 +96,14 @@ export async function sendEmail({ template }); + // 이메일 발신 이력 로깅 (최소 필드만 저장) + await createEmailLog({ + from: fromAddress, + to, + cc, + subject: renderedSubject, + }); + return result; } catch (error) { -- cgit v1.2.3