From 02b1cf005cf3e1df64183d20ba42930eb2767a9f Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 21 Aug 2025 06:57:36 +0000 Subject: (대표님, 최겸) 설계메뉴추가, 작업사항 업데이트 설계메뉴 - 문서관리 설계메뉴 - 벤더 데이터 gtc 메뉴 업데이트 정보시스템 - 메뉴리스트 및 정보 업데이트 파일 라우트 업데이트 엑셀임포트 개선 기본계약 개선 벤더 가입과정 변경 및 개선 벤더 기본정보 - pq 돌체 오류 수정 및 개선 벤더 로그인 과정 이메일 오류 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/information/repository.ts | 192 +++---- lib/information/service.ts | 618 +++++++++++---------- .../table/update-information-dialog.tsx | 380 ++++++++----- lib/information/validations.ts | 5 +- 4 files changed, 642 insertions(+), 553 deletions(-) (limited to 'lib/information') diff --git a/lib/information/repository.ts b/lib/information/repository.ts index f640a4c6..c7c000b1 100644 --- a/lib/information/repository.ts +++ b/lib/information/repository.ts @@ -1,125 +1,52 @@ -import { asc, desc, eq, ilike, and, count, sql } from "drizzle-orm" +import { asc, desc, eq, and } 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, pagePath, isActive, from, to } = input - - const conditions = [] - - if (pagePath) { - conditions.push(ilike(pageInformation.pagePath, `%${pagePath}%`)) - } - - if (isActive !== null && isActive !== undefined) { - conditions.push(eq(pageInformation.isActive, isActive)) - } - - if (from) { - conditions.push(sql`${pageInformation.createdAt} >= ${from}`) - } +import { + pageInformation, + informationAttachments, + type PageInformation, + type NewPageInformation, + type InformationAttachment, + type NewInformationAttachment +} from "@/db/schema/information" - 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); - } - } +// 인포메이션 수정 +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() - const whereClause = conditions.length > 0 ? and(...conditions) : undefined + return result[0] || null +} - const data = await db +// 인포메이션과 첨부파일 함께 조회 +export async function getInformationWithAttachments(id: number) { + const information = await db .select() .from(pageInformation) - .where(whereClause) - .orderBy(orderBy) - .limit(per_page) - .offset(offset) - - return data -} - -// 기존 패턴: 인포메이션 총 개수 조회 -export async function countInformation(input: GetInformationSchema) { - const { pagePath, isActive, from, to } = input - - const conditions = [] - - if (pagePath) { - conditions.push(ilike(pageInformation.pagePath, `%${pagePath}%`)) - } + .where(eq(pageInformation.id, id)) + .limit(1) - if (isActive !== null && isActive !== undefined) { - conditions.push(eq(pageInformation.isActive, isActive)) - } + if (!information[0]) return null - if (from) { - conditions.push(sql`${pageInformation.createdAt} >= ${from}`) - } + const attachments = await db + .select() + .from(informationAttachments) + .where(eq(informationAttachments.informationId, id)) + .orderBy(asc(informationAttachments.createdAt)) - if (to) { - conditions.push(sql`${pageInformation.createdAt} <= ${to}`) + return { + ...information[0], + attachments } - - 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 getInformationByPagePath(pagePath: string): Promise { - const result = await db +// 페이지 경로로 인포메이션과 첨부파일 함께 조회 +export async function getInformationByPagePathWithAttachments(pagePath: string) { + const information = await db .select() .from(pageInformation) .where(and( @@ -128,26 +55,55 @@ export async function getInformationByPagePath(pagePath: string): Promise): Promise { +// 첨부파일 추가 +export async function addInformationAttachment(data: NewInformationAttachment): Promise { const result = await db - .update(pageInformation) - .set({ ...data, updatedAt: new Date() }) - .where(eq(pageInformation.id, id)) + .insert(informationAttachments) + .values(data) .returning() return result[0] || null } -// ID로 인포메이션 조회 -export async function getInformationById(id: number): Promise { +// 첨부파일 삭제 +export async function deleteInformationAttachment(id: number): Promise { + const result = await db + .delete(informationAttachments) + .where(eq(informationAttachments.id, id)) + .returning() + + return result.length > 0 +} + +// 인포메이션 ID로 모든 첨부파일 조회 +export async function getAttachmentsByInformationId(informationId: number): Promise { + return await db + .select() + .from(informationAttachments) + .where(eq(informationAttachments.informationId, informationId)) + .orderBy(asc(informationAttachments.createdAt)) +} + +// 첨부파일 ID로 조회 +export async function getAttachmentById(id: number): Promise { const result = await db .select() - .from(pageInformation) - .where(eq(pageInformation.id, id)) + .from(informationAttachments) + .where(eq(informationAttachments.id, id)) .limit(1) return result[0] || null diff --git a/lib/information/service.ts b/lib/information/service.ts index 30a651f1..2826c0e9 100644 --- a/lib/information/service.ts +++ b/lib/information/service.ts @@ -1,283 +1,335 @@ -"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 { - UpdateInformationSchema, - GetInformationSchema -} from "./validations" - -import { - selectInformation, - countInformation, - getInformationByPagePath, - updateInformation, - getInformationById, - selectInformationLists, - countInformationLists -} from "./repository" - -import type { PageInformation } from "@/db/schema/information" - -// 최신 패턴: 고급 필터링과 캐싱을 지원하는 인포메이션 목록 조회 -export async function getInformationLists(input: GetInformationSchema) { - return unstable_cache( - async () => { - try { - // 고급 검색 로직 - const { page, perPage, search, filters, joinOperator, pagePath, pageName, informationContent, isActive } = input - - // 기본 검색 조건들 - const conditions = [] - - // 검색어가 있으면 여러 필드에서 검색 - if (search && search.trim()) { - const searchConditions = [ - ilike(pageInformation.pagePath, `%${search}%`), - ilike(pageInformation.pageName, `%${search}%`), - ilike(pageInformation.informationContent, `%${search}%`) - ] - conditions.push(or(...searchConditions)) - } - - // 개별 필드 조건들 - if (pagePath && pagePath.trim()) { - conditions.push(ilike(pageInformation.pagePath, `%${pagePath}%`)) - } - - if (pageName && pageName.trim()) { - conditions.push(ilike(pageInformation.pageName, `%${pageName}%`)) - } - - if (informationContent && informationContent.trim()) { - conditions.push(ilike(pageInformation.informationContent, `%${informationContent}%`)) - } - - if (isActive !== null && isActive !== undefined) { - conditions.push(eq(pageInformation.isActive, isActive)) - } - - // 고급 필터 처리 - if (filters && filters.length > 0) { - const advancedConditions = filters.map(() => - filterColumns({ - table: pageInformation, - filters: filters, - joinOperator: joinOperator, - }) - ) - - if (advancedConditions.length > 0) { - if (joinOperator === "or") { - conditions.push(or(...advancedConditions)) - } else { - conditions.push(and(...advancedConditions)) - } - } - } - - // 전체 WHERE 조건 조합 - const finalWhere = conditions.length > 0 - ? (joinOperator === "or" ? or(...conditions) : and(...conditions)) - : undefined - - // 페이지네이션 - const offset = (page - 1) * perPage - - // 정렬 처리 - 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 === "pagePath") { - return item.desc ? desc(pageInformation.pagePath) : asc(pageInformation.pagePath) - } else if (item.id === "pageName") { - return item.desc ? desc(pageInformation.pageName) : asc(pageInformation.pageName) - } else if (item.id === "informationContent") { - return item.desc ? desc(pageInformation.informationContent) : asc(pageInformation.informationContent) - } 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(pagePath: string): Promise { - try { - return await getInformationByPagePath(pagePath) - } catch (error) { - console.error(`Failed to get information for page ${pagePath}:`, error) - return null - } -} - -// 캐시된 페이지별 인포메이션 조회 -export const getCachedPageInformation = unstable_cache( - async (pagePath: string) => getPageInformation(pagePath), - ["page-information"], - { - tags: ["page-information"], - revalidate: 3600, // 1시간 캐시 - } -) - -// 인포메이션 수정 (내용과 첨부파일만) -export async function updateInformationData(input: UpdateInformationSchema) { - try { - const { id, ...updateData } = input - - // 수정 가능한 필드만 허용 - const allowedFields = { - informationContent: updateData.informationContent, - attachmentFilePath: updateData.attachmentFilePath, - attachmentFileName: updateData.attachmentFileName, - updatedAt: new Date() - } - - const result = await updateInformation(id, allowedFields) - - 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) - } - } -} - -// 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(pagePath: string, userId: string): Promise { - try { - // pagePath를 menuPath로 변환 (pagePath가 menuPath의 마지막 부분이라고 가정) - // 예: pagePath "vendor-list" -> menuPath "/evcp/vendor-list" 또는 "/partners/vendor-list" - const menuPathQueries = [ - `/evcp/${pagePath}`, - `/partners/${pagePath}`, - `/${pagePath}`, // 루트 경로 - pagePath // 정확한 매칭 - ] - - // menu_assignments에서 해당 pagePath와 매칭되는 메뉴 찾기 - 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 (pagePath: string, userId: string) => checkInformationEditPermission(pagePath, userId), - ["information-edit-permission"], - { - tags: ["information-edit-permission"], - revalidate: 300, // 5분 캐시 - } -) \ No newline at end of file +"use server" + +import { revalidateTag } from "next/cache" +import { getErrorMessage } from "@/lib/handle-error" +import { unstable_cache } from "@/lib/unstable-cache" +import { desc, or, eq } from "drizzle-orm" +import db from "@/db/db" +import { pageInformation, menuAssignments } from "@/db/schema" +import { saveDRMFile } from "@/lib/file-stroage" +import { decryptWithServerAction } from "@/components/drm/drmUtils" + +import type { + UpdateInformationSchema +} from "./validations" + +import { + getInformationByPagePathWithAttachments, + updateInformation, + getInformationWithAttachments, + addInformationAttachment, + deleteInformationAttachment, + getAttachmentById +} from "./repository" + +import type { PageInformation, InformationAttachment } from "@/db/schema/information" + +// 간단한 인포메이션 목록 조회 (페이지네이션 없이 전체 조회) +export async function getInformationLists() { + try { + // 전체 데이터 조회 (클라이언트에서 검색 처리) + const data = await db + .select() + .from(pageInformation) + .orderBy(desc(pageInformation.createdAt)) + + return { data } + } catch (err) { + console.error("Failed to get information lists:", err) + return { data: [] } + } +} + + + +// 페이지별 인포메이션 조회 (첨부파일 포함) +export async function getPageInformation(pagePath: string) { + try { + return await getInformationByPagePathWithAttachments(pagePath) + } catch (error) { + console.error(`Failed to get information for page ${pagePath}:`, error) + return null + } +} + +// 캐시된 페이지별 인포메이션 조회 +export const getCachedPageInformation = unstable_cache( + async (pagePath: string) => getPageInformation(pagePath), + ["page-information"], + { + tags: ["page-information"], + revalidate: 3600, // 1시간 캐시 + } +) + +// 인포메이션 수정 (내용과 첨부파일만) +export async function updateInformationData(input: UpdateInformationSchema) { + try { + const { id, ...updateData } = input + + // 수정 가능한 필드만 허용 + const allowedFields = { + informationContent: updateData.informationContent, + isActive: updateData.isActive, + updatedAt: new Date() + } + + const result = await updateInformation(id, allowedFields) + + 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) + } + } +} + +// ID로 인포메이션 조회 (첨부파일 포함) +export async function getInformationDetail(id: number) { + try { + return await getInformationWithAttachments(id) + } catch (error) { + console.error(`Failed to get information detail for id ${id}:`, error) + return null + } +} + +// 인포메이션 편집 권한 확인 +export async function checkInformationEditPermission(pagePath: string, userId: string): Promise { + try { + // pagePath를 menuPath로 변환 (pagePath가 menuPath의 마지막 부분이라고 가정) + // 예: pagePath "vendor-list" -> menuPath "/evcp/vendor-list" 또는 "/partners/vendor-list" + const menuPathQueries = [ + `/evcp/${pagePath}`, + `/partners/${pagePath}`, + `/${pagePath}`, // 루트 경로 + pagePath // 정확한 매칭 + ] + + // menu_assignments에서 해당 pagePath와 매칭되는 메뉴 찾기 + 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 (pagePath: string, userId: string) => checkInformationEditPermission(pagePath, userId), + ["information-edit-permission"], + { + tags: ["information-edit-permission"], + revalidate: 300, // 5분 캐시 + } +) + +// menu_assignments 기반으로 page_information 동기화 +export async function syncInformationFromMenuAssignments() { + try { + // menu_assignments에서 모든 메뉴 가져오기 + const menuItems = await db.select().from(menuAssignments); + + let processedCount = 0; + + // upsert를 사용하여 각 메뉴 항목 처리 + for (const menu of menuItems) { + try { + await db.insert(pageInformation) + .values({ + pagePath: menu.menuPath, + pageName: menu.menuTitle, + informationContent: "", + isActive: true // 기본값으로 활성화 + }) + .onConflictDoUpdate({ + target: pageInformation.pagePath, + set: { + pageName: menu.menuTitle, + updatedAt: new Date() + } + }); + processedCount++; + } catch (itemError: any) { + console.warn(`메뉴 항목 처리 실패: ${menu.menuPath}`, itemError); + continue; + } + } + + revalidateTag("information"); + + return { + success: true, + message: `페이지 정보 동기화 완료: ${processedCount}개 처리됨` + }; + } catch (error) { + console.error("Information 동기화 오류:", error); + return { + success: false, + message: "페이지 정보 동기화 중 오류가 발생했습니다." + }; + } +} + +// 첨부파일 업로드 +export async function uploadInformationAttachment(formData: FormData) { + try { + const informationId = parseInt(formData.get("informationId") as string) + const file = formData.get("file") as File + + if (!informationId || !file) { + return { + success: false, + message: "필수 매개변수가 누락되었습니다." + } + } + + // 파일 저장 + const saveResult = await saveDRMFile( + file, + decryptWithServerAction, + `information/${informationId}`, + // userId는 필요시 추가 + ) + + if (!saveResult.success) { + return { + success: false, + message: saveResult.error || "파일 저장에 실패했습니다." + } + } + + // DB에 첨부파일 정보 저장 + const attachment = await addInformationAttachment({ + informationId, + fileName: file.name, + filePath: saveResult.publicPath || "", + fileSize: saveResult.fileSize ? String(saveResult.fileSize) : String(file.size) + }) + + if (!attachment) { + return { + success: false, + message: "첨부파일 정보 저장에 실패했습니다." + } + } + + revalidateTag("page-information") + revalidateTag("information-lists") + + return { + success: true, + message: "첨부파일이 성공적으로 업로드되었습니다.", + data: attachment + } + } catch (error) { + console.error("Failed to upload attachment:", error) + return { + success: false, + message: getErrorMessage(error) + } + } +} + +// 첨부파일 삭제 +export async function deleteInformationAttachmentAction(attachmentId: number) { + try { + const attachment = await getAttachmentById(attachmentId) + + if (!attachment) { + return { + success: false, + message: "첨부파일을 찾을 수 없습니다." + } + } + + // DB에서 삭제 + const deleted = await deleteInformationAttachment(attachmentId) + + if (!deleted) { + return { + success: false, + message: "첨부파일 삭제에 실패했습니다." + } + } + + revalidateTag("page-information") + revalidateTag("information-lists") + + return { + success: true, + message: "첨부파일이 성공적으로 삭제되었습니다." + } + } catch (error) { + console.error("Failed to delete attachment:", error) + return { + success: false, + message: getErrorMessage(error) + } + } +} + +// 첨부파일 다운로드 +export async function downloadInformationAttachment(attachmentId: number) { + try { + const attachment = await getAttachmentById(attachmentId) + + if (!attachment) { + return { + success: false, + message: "첨부파일을 찾을 수 없습니다." + } + } + + // 파일 다운로드 (클라이언트에서 사용) + return { + success: true, + data: { + filePath: attachment.filePath, + fileName: attachment.fileName, + fileSize: attachment.fileSize + } + } + } catch (error) { + console.error("Failed to download attachment:", error) + return { + success: false, + message: getErrorMessage(error) + } + } +} \ No newline at end of file diff --git a/lib/information/table/update-information-dialog.tsx b/lib/information/table/update-information-dialog.tsx index b4c11e17..a02b6eb1 100644 --- a/lib/information/table/update-information-dialog.tsx +++ b/lib/information/table/update-information-dialog.tsx @@ -2,10 +2,12 @@ import * as React from "react" import { zodResolver } from "@hookform/resolvers/zod" -import { useForm } from "react-hook-form" +import { useForm, useFieldArray } from "react-hook-form" import { toast } from "sonner" -import { Loader, Upload, X } from "lucide-react" -import { useRouter } from "next/navigation" +import { Loader, Download, X, FileText } from "lucide-react" +import { useRouter, useParams } from "next/navigation" +import { useTranslation } from "@/i18n/client" +import { z } from "zod" import { Button } from "@/components/ui/button" import { @@ -24,17 +26,43 @@ import { FormLabel, FormMessage, } from "@/components/ui/form" +import { + Dropzone, + DropzoneZone, + DropzoneInput, + DropzoneUploadIcon, + DropzoneTitle, + DropzoneDescription, +} from "@/components/ui/dropzone" import { Textarea } from "@/components/ui/textarea" import { Switch } from "@/components/ui/switch" -import { updateInformationData } from "@/lib/information/service" -import { updateInformationSchema, type UpdateInformationSchema } from "@/lib/information/validations" -import type { PageInformation } from "@/db/schema/information" +import { + updateInformationData, + uploadInformationAttachment, + deleteInformationAttachmentAction, + downloadInformationAttachment +} from "@/lib/information/service" +import type { PageInformation, InformationAttachment } from "@/db/schema/information" +import { downloadFile } from "@/lib/file-download" +import prettyBytes from "pretty-bytes" + +const MAX_FILE_SIZE = 50 * 1024 * 1024 // 50MB + +// 폼 스키마 +const updateInformationSchema = z.object({ + id: z.number(), + informationContent: z.string().min(1, "인포메이션 내용을 입력해주세요"), + isActive: z.boolean(), + newFiles: z.array(z.any()).optional(), +}) + +type UpdateInformationSchema = z.infer interface UpdateInformationDialogProps { open: boolean onOpenChange: (open: boolean) => void - information?: PageInformation + information?: PageInformation & { attachments?: InformationAttachment[] } onSuccess?: () => void } @@ -45,137 +73,172 @@ export function UpdateInformationDialog({ onSuccess, }: UpdateInformationDialogProps) { const router = useRouter() + const params = useParams() + const lng = (params?.lng as string) || 'ko' + const { t } = useTranslation(lng, 'common') const [isLoading, setIsLoading] = React.useState(false) - const [uploadedFile, setUploadedFile] = React.useState(null) + const [isUploadingFiles, setIsUploadingFiles] = React.useState(false) + const [existingAttachments, setExistingAttachments] = React.useState([]) const form = useForm({ resolver: zodResolver(updateInformationSchema), defaultValues: { id: 0, informationContent: "", - attachmentFileName: "", - attachmentFilePath: "", - attachmentFileSize: "", isActive: true, + newFiles: [], }, }) + const { fields: newFileFields, append: appendFile, remove: removeFile } = useFieldArray({ + control: form.control, + name: "newFiles", + }) + // 인포메이션 데이터가 변경되면 폼 업데이트 React.useEffect(() => { if (information && open) { form.reset({ id: information.id, informationContent: information.informationContent || "", - attachmentFileName: information.attachmentFileName || "", - attachmentFilePath: information.attachmentFilePath || "", - attachmentFileSize: information.attachmentFileSize || "", isActive: information.isActive, + newFiles: [], }) + setExistingAttachments(information.attachments || []) } }, [information, open, form]) - 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 handleDropAccepted = React.useCallback((acceptedFiles: File[]) => { + acceptedFiles.forEach(file => { + appendFile(file) + }) + }, [appendFile]) - const response = await fetch("/api/upload", { - method: "POST", - body: formData, + const handleDropRejected = React.useCallback((rejectedFiles: any[]) => { + rejectedFiles.forEach(rejection => { + toast.error(`파일 업로드 실패: ${rejection.file.name}`) }) + }, []) - if (!response.ok) { - throw new Error("파일 업로드에 실패했습니다.") + // 기존 첨부파일 다운로드 + const handleDownloadAttachment = async (attachment: InformationAttachment) => { + try { + const result = await downloadInformationAttachment(attachment.id) + if (result.success && result.data) { + await downloadFile(result.data.filePath, result.data.fileName) + toast.success("파일 다운로드가 시작되었습니다.") + } else { + toast.error(result.message || "파일 다운로드에 실패했습니다.") + } + } catch (error) { + console.error("파일 다운로드 오류:", error) + toast.error("파일 다운로드 중 오류가 발생했습니다.") } + } + + // 기존 첨부파일 삭제 + const handleDeleteAttachment = async (attachmentId: number) => { + if (!confirm("정말로 이 첨부파일을 삭제하시겠습니까?")) return - const result = await response.json() - return result.url + try { + const result = await deleteInformationAttachmentAction(attachmentId) + if (result.success) { + setExistingAttachments(prev => prev.filter(att => att.id !== attachmentId)) + toast.success(result.message) + } else { + toast.error(result.message) + } + } catch (error) { + console.error("첨부파일 삭제 오류:", error) + toast.error("첨부파일 삭제 중 오류가 발생했습니다.") + } } const onSubmit = async (values: UpdateInformationSchema) => { setIsLoading(true) try { - const finalValues = { ...values } + // 1. 인포메이션 정보 업데이트 + const updateResult = await updateInformationData({ + id: values.id, + informationContent: values.informationContent, + isActive: values.isActive, + }) - // 새 파일이 있으면 업로드 - if (uploadedFile) { - const filePath = await uploadFile(uploadedFile) - finalValues.attachmentFilePath = filePath + if (!updateResult.success) { + toast.error(updateResult.message) + return } - const result = await updateInformationData(finalValues) - - if (result.success) { - toast.success(result.message) - if (onSuccess) onSuccess() - onOpenChange(false) - router.refresh() - } else { - toast.error(result.message) + // 2. 새 첨부파일 업로드 + if (values.newFiles && values.newFiles.length > 0) { + setIsUploadingFiles(true) + + for (const file of values.newFiles) { + const formData = new FormData() + formData.append("informationId", String(values.id)) + formData.append("file", file) + + const uploadResult = await uploadInformationAttachment(formData) + if (!uploadResult.success) { + toast.error(`파일 업로드 실패: ${file.name} - ${uploadResult.message}`) + } + } + setIsUploadingFiles(false) } + + toast.success("인포메이션이 성공적으로 수정되었습니다.") + if (onSuccess) onSuccess() + onOpenChange(false) + router.refresh() } catch (error) { + console.error("인포메이션 수정 오류:", error) toast.error("인포메이션 수정에 실패했습니다.") - console.error(error) } finally { setIsLoading(false) + setIsUploadingFiles(false) } } const handleClose = () => { - setUploadedFile(null) + form.reset() + setExistingAttachments([]) onOpenChange(false) } - const currentFileName = form.watch("attachmentFileName") - return ( - + - 인포메이션 수정 + {t('information.edit.title', '인포메이션 수정')} - 페이지 인포메이션 정보를 수정합니다. + {t('information.edit.description', '페이지 인포메이션 정보를 수정합니다.')}
- + + {/* 페이지 정보 */}
- 페이지 정보 + {t('information.page.info', '페이지 정보')}
-
-
페이지명: {information?.pageName}
-
경로: {information?.pagePath}
+
+
{t('information.page.name', '페이지명')}: {information?.pageName}
+
{t('information.page.path', '경로')}: {information?.pagePath}
+ {/* 인포메이션 내용 */} ( - 인포메이션 내용 + {t('information.content.label', '인포메이션 내용')}