From d66d308169e559457878c02e3b0443da22693241 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 1 Jul 2025 02:53:18 +0000 Subject: (최겸) 정보시스템 인포메이션 기능 개발 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[lng]/evcp/(evcp)/information/page.tsx | 45 +++ components/information/information-button.tsx | 259 ++++++++++++++ config/informationColumnsConfig.ts | 64 ++++ config/menuConfig.ts | 9 +- lib/information/repository.ts | 190 +++++++++++ lib/information/service.ts | 343 +++++++++++++++++++ lib/information/table/add-information-dialog.tsx | 329 ++++++++++++++++++ .../table/delete-information-dialog.tsx | 125 +++++++ .../table/information-table-columns.tsx | 248 ++++++++++++++ .../table/information-table-toolbar-actions.tsx | 25 ++ lib/information/table/information-table.tsx | 148 ++++++++ .../table/update-information-dialog.tsx | 376 +++++++++++++++++++++ lib/information/validations.ts | 89 +++++ lib/menu-list/servcie.ts | 5 +- 14 files changed, 2252 insertions(+), 3 deletions(-) create mode 100644 app/[lng]/evcp/(evcp)/information/page.tsx create mode 100644 components/information/information-button.tsx create mode 100644 config/informationColumnsConfig.ts create mode 100644 lib/information/repository.ts create mode 100644 lib/information/service.ts create mode 100644 lib/information/table/add-information-dialog.tsx create mode 100644 lib/information/table/delete-information-dialog.tsx create mode 100644 lib/information/table/information-table-columns.tsx create mode 100644 lib/information/table/information-table-toolbar-actions.tsx create mode 100644 lib/information/table/information-table.tsx create mode 100644 lib/information/table/update-information-dialog.tsx create mode 100644 lib/information/validations.ts diff --git a/app/[lng]/evcp/(evcp)/information/page.tsx b/app/[lng]/evcp/(evcp)/information/page.tsx new file mode 100644 index 00000000..4027ab8a --- /dev/null +++ b/app/[lng]/evcp/(evcp)/information/page.tsx @@ -0,0 +1,45 @@ +import * as React from "react" +import type { Metadata } from "next" +import { unstable_noStore as noStore } from "next/cache" + +import { Shell } from "@/components/shell" +import { getInformationLists } from "@/lib/information/service" +import { InformationTable } from "@/lib/information/table/information-table" +import { searchParamsInformationCache } from "@/lib/information/validations" +import type { SearchParams } from "@/types/table" +import { InformationButton } from "@/components/information/information-button" + +export const metadata: Metadata = { + title: "인포메이션 관리", + description: "페이지별 도움말 및 첨부파일을 관리합니다.", +} + +interface InformationPageProps { + searchParams: Promise +} + +export default async function InformationPage({ searchParams }: InformationPageProps) { + noStore() + + const search = await searchParamsInformationCache.parse(await searchParams) + + const informationPromise = getInformationLists(search) + + return ( + +
+
+
+
+

+ 도움말 관리 +

+ +
+
+
+
+ +
+ ) +} \ No newline at end of file diff --git a/components/information/information-button.tsx b/components/information/information-button.tsx new file mode 100644 index 00000000..da0de548 --- /dev/null +++ b/components/information/information-button.tsx @@ -0,0 +1,259 @@ +"use client" + +import React, { useState, useEffect } from "react" +import { Info, Download, Edit } from "lucide-react" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { getCachedPageInformation, getCachedEditPermission } from "@/lib/information/service" +import { UpdateInformationDialog } from "@/lib/information/table/update-information-dialog" +import type { PageInformation } from "@/db/schema/information" +import { useSession } from "next-auth/react" + +interface InformationButtonProps { + pageCode: string + className?: string + variant?: "default" | "outline" | "ghost" | "secondary" + size?: "default" | "sm" | "lg" | "icon" +} + +export function InformationButton({ + pageCode, + className, + variant = "ghost", + size = "icon" +}: InformationButtonProps) { + const { data: session } = useSession() + const [information, setInformation] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const [isOpen, setIsOpen] = useState(false) + const [hasEditPermission, setHasEditPermission] = useState(false) + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) + + useEffect(() => { + if (isOpen && !information) { + loadInformation() + } + }, [isOpen, information]) + + // 편집 권한 확인 + useEffect(() => { + const checkEditPermission = async () => { + if (session?.user?.id) { + try { + const permission = await getCachedEditPermission(pageCode, session.user.id) + setHasEditPermission(permission) + } catch (error) { + console.error("Failed to check edit permission:", error) + setHasEditPermission(false) + } + } + } + + checkEditPermission() + }, [pageCode, session?.user?.id]) + + const loadInformation = async () => { + setIsLoading(true) + try { + const data = await getCachedPageInformation(pageCode) + setInformation(data) + } catch (error) { + console.error("Failed to load information:", error) + } finally { + setIsLoading(false) + } + } + + const handleDownload = () => { + if (information?.attachmentFilePath && information?.attachmentFileName) { + const link = document.createElement('a') + link.href = information.attachmentFilePath + link.download = information.attachmentFileName + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + } + } + + const handleEditClick = () => { + setIsEditDialogOpen(true) + } + + const handleEditClose = () => { + setIsEditDialogOpen(false) + refreshInformation() + } + + const refreshInformation = () => { + // 편집 후 정보 다시 로드 + setInformation(null) + if (isOpen) { + loadInformation() + } + // 캐시 무효화를 위해 다시 확인 + setTimeout(() => { + loadInformation() + }, 500) + } + + // 인포메이션이 없으면 버튼을 숨김 + const [hasInformation, setHasInformation] = useState(null) + + useEffect(() => { + const checkInformation = async () => { + try { + const data = await getCachedPageInformation(pageCode) + setHasInformation(!!data) + } catch { + setHasInformation(false) + } + } + checkInformation() + }, [pageCode]) + + // 인포메이션이 없으면 버튼을 숨김 + if (hasInformation === false) { + return null + } + + return ( + <> + + + + + + +
+
+ {/* */} +
+ {information?.title || "페이지 정보"} + {information?.pageName} +
+
+ {hasEditPermission && ( + + )} +
+
+ +
+ {isLoading ? ( +
+
+
+ ) : information ? ( + <> + {/* 공지사항 */} + {(information.noticeTitle || information.noticeContent) && ( +
+
+

공지사항

+
+
+ {information.noticeTitle && ( +
+ 제목: {information.noticeTitle} +
+ )} + {information.noticeContent && ( +
+
+ {information.noticeContent} +
+
+ )} +
+
+ )} + + {/* 페이지 정보 */} +
+

도움말

+
+
+ {information.description || "페이지 설명이 없습니다."} +
+
+
+ + {/* 첨부파일 */} +
+

첨부파일

+ {information.attachmentFileName ? ( +
+
+
+
+ {information.attachmentFileName} +
+ {information.attachmentFileSize && ( +
+ {information.attachmentFileSize} +
+ )} +
+ +
+
+ ) : ( +
+ +

첨부된 파일이 없습니다.

+
+ )} +
+ + ) : ( +
+ 이 페이지에 대한 정보가 없습니다. +
+ )} +
+
+
+ + {/* 편집 다이얼로그 */} + {information && ( + + )} + + ) +} \ No newline at end of file diff --git a/config/informationColumnsConfig.ts b/config/informationColumnsConfig.ts new file mode 100644 index 00000000..6357cfa3 --- /dev/null +++ b/config/informationColumnsConfig.ts @@ -0,0 +1,64 @@ +import { PageInformation } from "@/db/schema/information" + +export interface InformationColumnConfig { + id: keyof PageInformation + label: string + group?: string + excelHeader?: string + type?: string +} + +export const informationColumnsConfig: InformationColumnConfig[] = [ + { + id: "pageCode", + label: "페이지 코드", + excelHeader: "페이지 코드", + }, + { + id: "pageName", + label: "페이지명", + excelHeader: "페이지명", + }, + { + id: "title", + label: "제목", + excelHeader: "제목", + }, + { + id: "description", + label: "설명", + excelHeader: "설명", + }, + { + id: "noticeTitle", + label: "공지사항 제목", + excelHeader: "공지사항 제목", + }, + { + id: "noticeContent", + label: "공지사항 내용", + excelHeader: "공지사항 내용", + }, + { + id: "attachmentFileName", + label: "첨부파일", + excelHeader: "첨부파일", + }, + { + id: "isActive", + label: "상태", + excelHeader: "상태", + }, + { + id: "createdAt", + label: "생성일", + excelHeader: "생성일", + type: "date", + }, + { + id: "updatedAt", + label: "수정일", + excelHeader: "수정일", + type: "date", + }, +] \ No newline at end of file diff --git a/config/menuConfig.ts b/config/menuConfig.ts index 864b5dc9..d9b272e1 100644 --- a/config/menuConfig.ts +++ b/config/menuConfig.ts @@ -341,17 +341,22 @@ export const mainNav: MenuSection[] = [ title: "정보시스템", useGrouping: true, // 그룹핑 적용 items: [ + { + title: "인포메이션 관리", + href: "/evcp/information", + group: "정보시스템" + }, { title: "메뉴 리스트", href: "/evcp/menu-list", // icon: "FileText", - // group: "인터페이스" + group: "메뉴" }, { title: "메뉴 접근제어", href: "/evcp/menu-access", // icon: "FileText", - // group: "인터페이스" + group: "메뉴" }, { title: "인터페이스 목록 관리", diff --git a/lib/information/repository.ts b/lib/information/repository.ts new file mode 100644 index 00000000..2a3bc1c0 --- /dev/null +++ b/lib/information/repository.ts @@ -0,0 +1,190 @@ +import { asc, desc, eq, ilike, and, count, sql } from "drizzle-orm" +import db from "@/db/db" +import { pageInformation, type PageInformation, type NewPageInformation } from "@/db/schema/information" +import type { GetInformationSchema } from "./validations" +import { PgTransaction } from "drizzle-orm/pg-core" + +// 최신 패턴: 트랜잭션을 지원하는 인포메이션 조회 +export async function selectInformationLists( + tx: PgTransaction, + params: { + where?: ReturnType + orderBy?: (ReturnType | ReturnType)[] + offset?: number + limit?: number + } +) { + const { where, orderBy, offset = 0, limit = 10 } = params + + return tx + .select() + .from(pageInformation) + .where(where) + .orderBy(...(orderBy ?? [desc(pageInformation.createdAt)])) + .offset(offset) + .limit(limit) +} + +// 최신 패턴: 트랜잭션을 지원하는 카운트 조회 +export async function countInformationLists( + tx: PgTransaction, + where?: ReturnType +) { + const res = await tx + .select({ count: count() }) + .from(pageInformation) + .where(where) + + return res[0]?.count ?? 0 +} + +// 기존 패턴 (하위 호환성을 위해 유지) +export async function selectInformation(input: GetInformationSchema) { + const { page, per_page = 50, sort, pageCode, pageName, isActive, from, to } = input + + const conditions = [] + + if (pageCode) { + conditions.push(ilike(pageInformation.pageCode, `%${pageCode}%`)) + } + + if (pageName) { + conditions.push(ilike(pageInformation.pageName, `%${pageName}%`)) + } + + if (isActive !== null) { + conditions.push(eq(pageInformation.isActive, isActive)) + } + + if (from) { + conditions.push(sql`${pageInformation.createdAt} >= ${from}`) + } + + if (to) { + conditions.push(sql`${pageInformation.createdAt} <= ${to}`) + } + + const offset = (page - 1) * per_page + + // 정렬 설정 + let orderBy = desc(pageInformation.createdAt); + + if (sort && Array.isArray(sort) && sort.length > 0) { + const sortItem = sort[0]; + if (sortItem.id === "createdAt") { + orderBy = sortItem.desc ? desc(pageInformation.createdAt) : asc(pageInformation.createdAt); + } + } + + const whereClause = conditions.length > 0 ? and(...conditions) : undefined + + const data = await db + .select() + .from(pageInformation) + .where(whereClause) + .orderBy(orderBy) + .limit(per_page) + .offset(offset) + + return data +} + +// 기존 패턴: 인포메이션 총 개수 조회 +export async function countInformation(input: GetInformationSchema) { + const { pageCode, pageName, isActive, from, to } = input + + const conditions = [] + + if (pageCode) { + conditions.push(ilike(pageInformation.pageCode, `%${pageCode}%`)) + } + + if (pageName) { + conditions.push(ilike(pageInformation.pageName, `%${pageName}%`)) + } + + if (isActive !== null) { + conditions.push(eq(pageInformation.isActive, isActive)) + } + + if (from) { + conditions.push(sql`${pageInformation.createdAt} >= ${from}`) + } + + if (to) { + conditions.push(sql`${pageInformation.createdAt} <= ${to}`) + } + + const whereClause = conditions.length > 0 ? and(...conditions) : undefined + + const result = await db + .select({ count: count() }) + .from(pageInformation) + .where(whereClause) + + return result[0]?.count ?? 0 +} + +// 페이지 코드별 인포메이션 조회 (활성화된 것만) +export async function getInformationByPageCode(pageCode: string): Promise { + const result = await db + .select() + .from(pageInformation) + .where(and( + eq(pageInformation.pageCode, pageCode), + eq(pageInformation.isActive, true) + )) + .limit(1) + + return result[0] || null +} + +// 인포메이션 생성 +export async function insertInformation(data: NewPageInformation): Promise { + const result = await db + .insert(pageInformation) + .values(data) + .returning() + + return result[0] +} + +// 인포메이션 수정 +export async function updateInformation(id: number, data: Partial): Promise { + const result = await db + .update(pageInformation) + .set({ ...data, updatedAt: new Date() }) + .where(eq(pageInformation.id, id)) + .returning() + + return result[0] || null +} + +// 인포메이션 삭제 +export async function deleteInformationById(id: number): Promise { + const result = await db + .delete(pageInformation) + .where(eq(pageInformation.id, id)) + + return (result.rowCount ?? 0) > 0 +} + +// 인포메이션 다중 삭제 +export async function deleteInformationByIds(ids: number[]): Promise { + const result = await db + .delete(pageInformation) + .where(sql`${pageInformation.id} = ANY(${ids})`) + + return result.rowCount ?? 0 +} + +// ID로 인포메이션 조회 +export async function getInformationById(id: number): Promise { + const result = await db + .select() + .from(pageInformation) + .where(eq(pageInformation.id, id)) + .limit(1) + + return result[0] || null +} \ No newline at end of file diff --git a/lib/information/service.ts b/lib/information/service.ts new file mode 100644 index 00000000..8f1e5679 --- /dev/null +++ b/lib/information/service.ts @@ -0,0 +1,343 @@ +"use server" + +import { revalidateTag, unstable_noStore } from "next/cache" +import { getErrorMessage } from "@/lib/handle-error" +import { unstable_cache } from "@/lib/unstable-cache" +import { filterColumns } from "@/lib/filter-columns" +import { asc, desc, ilike, and, or, eq } from "drizzle-orm" +import db from "@/db/db" +import { pageInformation, menuAssignments } from "@/db/schema" + +import type { + CreateInformationSchema, + UpdateInformationSchema, + GetInformationSchema +} from "./validations" + +import { + selectInformation, + countInformation, + getInformationByPageCode, + insertInformation, + updateInformation, + deleteInformationById, + deleteInformationByIds, + getInformationById, + selectInformationLists, + countInformationLists +} from "./repository" + +import type { PageInformation } from "@/db/schema/information" + +// 최신 패턴: 고급 필터링과 캐싱을 지원하는 인포메이션 목록 조회 +export async function getInformationLists(input: GetInformationSchema) { + return unstable_cache( + async () => { + try { + const offset = (input.page - 1) * input.perPage + + // 고급 필터링 + const advancedWhere = filterColumns({ + table: pageInformation, + filters: input.filters, + joinOperator: input.joinOperator, + }) + + // 전역 검색 + let globalWhere + if (input.search) { + const s = `%${input.search}%` + globalWhere = or( + ilike(pageInformation.pageCode, s), + ilike(pageInformation.pageName, s), + ilike(pageInformation.title, s), + ilike(pageInformation.description, s) + ) + } + + // 기본 필터들 + let basicWhere + const basicConditions = [] + + if (input.pageCode) { + basicConditions.push(ilike(pageInformation.pageCode, `%${input.pageCode}%`)) + } + + if (input.pageName) { + basicConditions.push(ilike(pageInformation.pageName, `%${input.pageName}%`)) + } + + if (input.title) { + basicConditions.push(ilike(pageInformation.title, `%${input.title}%`)) + } + + if (input.isActive !== undefined && input.isActive !== null) { + basicConditions.push(eq(pageInformation.isActive, input.isActive)) + } + + if (basicConditions.length > 0) { + basicWhere = and(...basicConditions) + } + + // 최종 where 조건 + const finalWhere = and( + advancedWhere, + globalWhere, + basicWhere + ) + + // 정렬 처리 + const orderBy = input.sort.length > 0 + ? input.sort.map((item) => { + if (item.id === "createdAt") { + return item.desc ? desc(pageInformation.createdAt) : asc(pageInformation.createdAt) + } else if (item.id === "updatedAt") { + return item.desc ? desc(pageInformation.updatedAt) : asc(pageInformation.updatedAt) + } else if (item.id === "pageCode") { + return item.desc ? desc(pageInformation.pageCode) : asc(pageInformation.pageCode) + } else if (item.id === "pageName") { + return item.desc ? desc(pageInformation.pageName) : asc(pageInformation.pageName) + } else if (item.id === "title") { + return item.desc ? desc(pageInformation.title) : asc(pageInformation.title) + } else if (item.id === "isActive") { + return item.desc ? desc(pageInformation.isActive) : asc(pageInformation.isActive) + } else { + return desc(pageInformation.createdAt) // 기본값 + } + }) + : [desc(pageInformation.createdAt)] + + // 트랜잭션 내부에서 Repository 호출 + const { data, total } = await db.transaction(async (tx) => { + const data = await selectInformationLists(tx, { + where: finalWhere, + orderBy, + offset, + limit: input.perPage, + }) + + const total = await countInformationLists(tx, finalWhere) + return { data, total } + }) + + const pageCount = Math.ceil(total / input.perPage) + + return { data, pageCount, total } + } catch (err) { + console.error("Failed to get information lists:", err) + // 에러 발생 시 기본값 반환 + return { data: [], pageCount: 0, total: 0 } + } + }, + [JSON.stringify(input)], // 캐싱 키 + { + revalidate: 3600, + tags: ["information-lists"], + } + )() +} + +// 기존 패턴 (하위 호환성을 위해 유지) +export async function getInformationList(input: Partial & { page: number; per_page: number }) { + unstable_noStore() + + try { + const [data, total] = await Promise.all([ + selectInformation(input as Parameters[0]), + countInformation(input as Parameters[0]) + ]) + + const pageCount = Math.ceil(total / input.per_page) + + return { + data, + pageCount, + total + } + } catch (error) { + console.error("Failed to get information list:", error) + throw new Error(getErrorMessage(error)) + } +} + +// 페이지별 인포메이션 조회 (일반 사용자용) +export async function getPageInformation(pageCode: string): Promise { + try { + return await getInformationByPageCode(pageCode) + } catch (error) { + console.error(`Failed to get information for page ${pageCode}:`, error) + return null + } +} + +// 캐시된 페이지별 인포메이션 조회 +export const getCachedPageInformation = unstable_cache( + async (pageCode: string) => getPageInformation(pageCode), + ["page-information"], + { + tags: ["page-information"], + revalidate: 3600, // 1시간 캐시 + } +) + +// 인포메이션 생성 +export async function createInformation(input: CreateInformationSchema) { + try { + const result = await insertInformation(input) + + revalidateTag("page-information") + revalidateTag("information-lists") + revalidateTag("information-edit-permission") + + return { + success: true, + data: result, + message: "인포메이션이 성공적으로 생성되었습니다." + } + } catch (error) { + console.error("Failed to create information:", error) + return { + success: false, + message: getErrorMessage(error) + } + } +} + +// 인포메이션 수정 +export async function updateInformationData(input: UpdateInformationSchema) { + try { + const { id, ...updateData } = input + const result = await updateInformation(id, updateData) + + if (!result) { + return { + success: false, + message: "인포메이션을 찾을 수 없거나 수정에 실패했습니다." + } + } + + revalidateTag("page-information") + revalidateTag("information-lists") + revalidateTag("information-edit-permission") // 편집 권한 캐시 무효화 + + return { + success: true, + message: "인포메이션이 성공적으로 수정되었습니다." + } + } catch (error) { + console.error("Failed to update information:", error) + return { + success: false, + message: getErrorMessage(error) + } + } +} + +// 인포메이션 삭제 +export async function deleteInformation(id: number) { + try { + const success = await deleteInformationById(id) + + if (!success) { + return { + success: false, + message: "인포메이션을 찾을 수 없거나 삭제에 실패했습니다." + } + } + + revalidateTag("page-information") + revalidateTag("information-lists") + + return { + success: true, + message: "인포메이션이 성공적으로 삭제되었습니다." + } + } catch (error) { + console.error("Failed to delete information:", error) + return { + success: false, + message: getErrorMessage(error) + } + } +} + +// 인포메이션 다중 삭제 +export async function deleteMultipleInformation(ids: number[]) { + try { + const deletedCount = await deleteInformationByIds(ids) + + revalidateTag("page-information") + revalidateTag("information-lists") + + return { + success: true, + deletedCount, + message: `${deletedCount}개의 인포메이션이 성공적으로 삭제되었습니다.` + } + } catch (error) { + console.error("Failed to delete multiple information:", error) + return { + success: false, + message: getErrorMessage(error) + } + } +} + +// ID로 인포메이션 조회 +export async function getInformationDetail(id: number): Promise { + try { + return await getInformationById(id) + } catch (error) { + console.error(`Failed to get information detail for id ${id}:`, error) + return null + } +} + +// 인포메이션 편집 권한 확인 +export async function checkInformationEditPermission(pageCode: string, userId: string): Promise { + try { + // pageCode를 menuPath로 변환 (pageCode가 menuPath의 마지막 부분이라고 가정) + // 예: pageCode "vendor-list" -> menuPath "/evcp/vendor-list" 또는 "/partners/vendor-list" + const menuPathQueries = [ + `/evcp/${pageCode}`, + `/partners/${pageCode}`, + `/${pageCode}`, // 루트 경로 + pageCode // 정확한 매칭 + ] + + // menu_assignments에서 해당 pageCode와 매칭되는 메뉴 찾기 + const menuAssignment = await db + .select() + .from(menuAssignments) + .where( + or( + ...menuPathQueries.map(path => eq(menuAssignments.menuPath, path)) + ) + ) + .limit(1) + + if (menuAssignment.length === 0) { + // 매칭되는 메뉴가 없으면 권한 없음 + return false + } + + const assignment = menuAssignment[0] + const userIdNumber = parseInt(userId) + + // 현재 사용자가 manager1 또는 manager2인지 확인 + return assignment.manager1Id === userIdNumber || assignment.manager2Id === userIdNumber + } catch (error) { + console.error("Failed to check information edit permission:", error) + return false + } +} + +// 캐시된 권한 확인 +export const getCachedEditPermission = unstable_cache( + async (pageCode: string, userId: string) => checkInformationEditPermission(pageCode, userId), + ["information-edit-permission"], + { + tags: ["information-edit-permission"], + revalidate: 300, // 5분 캐시 + } +) \ No newline at end of file diff --git a/lib/information/table/add-information-dialog.tsx b/lib/information/table/add-information-dialog.tsx new file mode 100644 index 00000000..a879fbfe --- /dev/null +++ b/lib/information/table/add-information-dialog.tsx @@ -0,0 +1,329 @@ +"use client" + +import * as React from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { toast } from "sonner" +import { Loader, Upload, X } from "lucide-react" +import { useRouter } from "next/navigation" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Switch } from "@/components/ui/switch" +import { createInformation } from "@/lib/information/service" +import { createInformationSchema, type CreateInformationSchema } from "@/lib/information/validations" + +interface AddInformationDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export function AddInformationDialog({ + open, + onOpenChange, +}: AddInformationDialogProps) { + const router = useRouter() + const [isLoading, setIsLoading] = React.useState(false) + const [uploadedFile, setUploadedFile] = React.useState(null) + + const form = useForm({ + resolver: zodResolver(createInformationSchema), + defaultValues: { + pageCode: "", + pageName: "", + title: "", + description: "", + noticeTitle: "", + noticeContent: "", + attachmentFileName: "", + attachmentFilePath: "", + attachmentFileSize: "", + isActive: true, + }, + }) + + const handleFileSelect = (event: React.ChangeEvent) => { + const file = event.target.files?.[0] + if (file) { + setUploadedFile(file) + // 파일 크기를 MB 단위로 변환 + const sizeInMB = (file.size / (1024 * 1024)).toFixed(2) + form.setValue("attachmentFileName", file.name) + form.setValue("attachmentFileSize", `${sizeInMB} MB`) + } + } + + const removeFile = () => { + setUploadedFile(null) + form.setValue("attachmentFileName", "") + form.setValue("attachmentFilePath", "") + form.setValue("attachmentFileSize", "") + } + + const uploadFile = async (file: File): Promise => { + const formData = new FormData() + formData.append("file", file) + + const response = await fetch("/api/upload", { + method: "POST", + body: formData, + }) + + if (!response.ok) { + throw new Error("파일 업로드에 실패했습니다.") + } + + const result = await response.json() + return result.url + } + + const onSubmit = async (values: CreateInformationSchema) => { + setIsLoading(true) + try { + const finalValues = { ...values } + + // 파일이 있으면 업로드 + if (uploadedFile) { + const filePath = await uploadFile(uploadedFile) + finalValues.attachmentFilePath = filePath + } + + const result = await createInformation(finalValues) + + if (result.success) { + toast.success(result.message) + form.reset() + setUploadedFile(null) + onOpenChange(false) + router.refresh() + } else { + toast.error(result.message) + } + } catch (error) { + toast.error("인포메이션 생성에 실패했습니다.") + console.error(error) + } finally { + setIsLoading(false) + } + } + + // 다이얼로그가 닫힐 때 폼 초기화 + React.useEffect(() => { + if (!open) { + form.reset() + setUploadedFile(null) + } + }, [open, form]) + + return ( + + + + 인포메이션 추가 + + 새로운 페이지 인포메이션을 추가합니다. + + + +
+ +
+ ( + + 페이지 코드 + + + + + + )} + /> + + ( + + 페이지명 + + + + + + )} + /> +
+ + ( + + 제목 + + + + + + )} + /> + + ( + + 설명 + +