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 --- app/[lng]/evcp/(evcp)/email-log/page.tsx | 59 +++++++++++++++++++++ db/schema/emailLogs.ts | 12 +++++ db/schema/index.ts | 4 +- 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 ++++ 9 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 app/[lng]/evcp/(evcp)/email-log/page.tsx create mode 100644 db/schema/emailLogs.ts 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 diff --git a/app/[lng]/evcp/(evcp)/email-log/page.tsx b/app/[lng]/evcp/(evcp)/email-log/page.tsx new file mode 100644 index 00000000..b73674e4 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/email-log/page.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { type Metadata } from "next" +import { Shell } from "@/components/shell" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { SearchParams } from "@/types/table" +import { SearchParamsEmailLogCache } from "@/lib/email-log/validations" +import { getEmailLogList } from "@/lib/email-log/service" +import { EmailLogTable } from "@/lib/email-log/table/email-log-table" + +export const metadata: Metadata = { + title: "이메일 발신 이력 조회", + description: "발신 이력을 조회합니다.", +} + +interface EmailLogPageProps { + searchParams: SearchParams +} + +export default async function EmailLogPage(props: EmailLogPageProps) { + const searchParams = await props.searchParams + const search = SearchParamsEmailLogCache.parse(searchParams) + + const promises = Promise.all([ + getEmailLogList(search), + ]) + + return ( + +
+
+
+
+

이메일 발신 이력 조회

+
+
+
+
+ + }> + + + } + > + + +
+ ) +} + + diff --git a/db/schema/emailLogs.ts b/db/schema/emailLogs.ts new file mode 100644 index 00000000..3a6f83dc --- /dev/null +++ b/db/schema/emailLogs.ts @@ -0,0 +1,12 @@ +import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"; + +export const emailLogs = pgTable("email_logs", { + id: serial("id").primaryKey(), + from: text("from").notNull(), + to: text("to").notNull(), + cc: text("cc"), + subject: text("subject").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), +}); + + diff --git a/db/schema/index.ts b/db/schema/index.ts index a223f0de..efd38e71 100644 --- a/db/schema/index.ts +++ b/db/schema/index.ts @@ -76,4 +76,6 @@ export * from './S_ERP/s_erp'; // AVL 스키마 export * from './avl/avl'; -export * from './avl/vendor-pool'; \ No newline at end of file +export * from './avl/vendor-pool'; +// === Email Logs 스키마 === +export * from './emailLogs'; \ No newline at end of file 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