From 4c15b99d9586aa48693213c78c02fba4639ebb85 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 1 Jul 2025 11:47:47 +0000 Subject: (최겸) 인포메이션 기능 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/information/repository.ts | 58 +--- lib/information/service.ts | 224 +++++--------- 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 | 124 ++------ lib/information/validations.ts | 32 +- 9 files changed, 124 insertions(+), 1189 deletions(-) delete mode 100644 lib/information/table/add-information-dialog.tsx delete mode 100644 lib/information/table/delete-information-dialog.tsx delete mode 100644 lib/information/table/information-table-columns.tsx delete mode 100644 lib/information/table/information-table-toolbar-actions.tsx delete mode 100644 lib/information/table/information-table.tsx (limited to 'lib/information') diff --git a/lib/information/repository.ts b/lib/information/repository.ts index 2a3bc1c0..f640a4c6 100644 --- a/lib/information/repository.ts +++ b/lib/information/repository.ts @@ -40,19 +40,15 @@ export async function countInformationLists( // 기존 패턴 (하위 호환성을 위해 유지) export async function selectInformation(input: GetInformationSchema) { - const { page, per_page = 50, sort, pageCode, pageName, isActive, from, to } = input + const { page, per_page = 50, sort, pagePath, isActive, from, to } = input const conditions = [] - if (pageCode) { - conditions.push(ilike(pageInformation.pageCode, `%${pageCode}%`)) + if (pagePath) { + conditions.push(ilike(pageInformation.pagePath, `%${pagePath}%`)) } - if (pageName) { - conditions.push(ilike(pageInformation.pageName, `%${pageName}%`)) - } - - if (isActive !== null) { + if (isActive !== null && isActive !== undefined) { conditions.push(eq(pageInformation.isActive, isActive)) } @@ -91,19 +87,15 @@ export async function selectInformation(input: GetInformationSchema) { // 기존 패턴: 인포메이션 총 개수 조회 export async function countInformation(input: GetInformationSchema) { - const { pageCode, pageName, isActive, from, to } = input + const { pagePath, isActive, from, to } = input const conditions = [] - if (pageCode) { - conditions.push(ilike(pageInformation.pageCode, `%${pageCode}%`)) + if (pagePath) { + conditions.push(ilike(pageInformation.pagePath, `%${pagePath}%`)) } - if (pageName) { - conditions.push(ilike(pageInformation.pageName, `%${pageName}%`)) - } - - if (isActive !== null) { + if (isActive !== null && isActive !== undefined) { conditions.push(eq(pageInformation.isActive, isActive)) } @@ -125,13 +117,13 @@ export async function countInformation(input: GetInformationSchema) { return result[0]?.count ?? 0 } -// 페이지 코드별 인포메이션 조회 (활성화된 것만) -export async function getInformationByPageCode(pageCode: string): Promise { +// 페이지 경로별 인포메이션 조회 (활성화된 것만) +export async function getInformationByPagePath(pagePath: string): Promise { const result = await db .select() .from(pageInformation) .where(and( - eq(pageInformation.pageCode, pageCode), + eq(pageInformation.pagePath, pagePath), eq(pageInformation.isActive, true) )) .limit(1) @@ -139,16 +131,6 @@ export async function getInformationByPageCode(pageCode: string): 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 @@ -160,24 +142,6 @@ export async function updateInformation(id: number, data: Partial { - 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 diff --git a/lib/information/service.ts b/lib/information/service.ts index 8f1e5679..30a651f1 100644 --- a/lib/information/service.ts +++ b/lib/information/service.ts @@ -9,7 +9,6 @@ import db from "@/db/db" import { pageInformation, menuAssignments } from "@/db/schema" import type { - CreateInformationSchema, UpdateInformationSchema, GetInformationSchema } from "./validations" @@ -17,11 +16,8 @@ import type { import { selectInformation, countInformation, - getInformationByPageCode, - insertInformation, + getInformationByPagePath, updateInformation, - deleteInformationById, - deleteInformationByIds, getInformationById, selectInformationLists, countInformationLists @@ -34,57 +30,65 @@ export async function getInformationLists(input: GetInformationSchema) { return unstable_cache( async () => { try { - const offset = (input.page - 1) * input.perPage + // 고급 검색 로직 + const { page, perPage, search, filters, joinOperator, pagePath, pageName, informationContent, isActive } = input - // 고급 필터링 - const advancedWhere = filterColumns({ - table: pageInformation, - filters: input.filters, - joinOperator: input.joinOperator, - }) + // 기본 검색 조건들 + const conditions = [] - // 전역 검색 - 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) - ) + // 검색어가 있으면 여러 필드에서 검색 + if (search && search.trim()) { + const searchConditions = [ + ilike(pageInformation.pagePath, `%${search}%`), + ilike(pageInformation.pageName, `%${search}%`), + ilike(pageInformation.informationContent, `%${search}%`) + ] + conditions.push(or(...searchConditions)) } - // 기본 필터들 - let basicWhere - const basicConditions = [] - - if (input.pageCode) { - basicConditions.push(ilike(pageInformation.pageCode, `%${input.pageCode}%`)) + // 개별 필드 조건들 + if (pagePath && pagePath.trim()) { + conditions.push(ilike(pageInformation.pagePath, `%${pagePath}%`)) } - - if (input.pageName) { - basicConditions.push(ilike(pageInformation.pageName, `%${input.pageName}%`)) + + if (pageName && pageName.trim()) { + conditions.push(ilike(pageInformation.pageName, `%${pageName}%`)) } - - if (input.title) { - basicConditions.push(ilike(pageInformation.title, `%${input.title}%`)) + + if (informationContent && informationContent.trim()) { + conditions.push(ilike(pageInformation.informationContent, `%${informationContent}%`)) } - - 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 - ) + + 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 @@ -93,12 +97,12 @@ export async function getInformationLists(input: GetInformationSchema) { 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 === "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 === "title") { - return item.desc ? desc(pageInformation.title) : asc(pageInformation.title) + } 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 { @@ -129,7 +133,7 @@ export async function getInformationLists(input: GetInformationSchema) { return { data: [], pageCount: 0, total: 0 } } }, - [JSON.stringify(input)], // 캐싱 키 + [JSON.stringify(input)], { revalidate: 3600, tags: ["information-lists"], @@ -161,18 +165,18 @@ export async function getInformationList(input: Partial & } // 페이지별 인포메이션 조회 (일반 사용자용) -export async function getPageInformation(pageCode: string): Promise { +export async function getPageInformation(pagePath: string): Promise { try { - return await getInformationByPageCode(pageCode) + return await getInformationByPagePath(pagePath) } catch (error) { - console.error(`Failed to get information for page ${pageCode}:`, error) + console.error(`Failed to get information for page ${pagePath}:`, error) return null } } // 캐시된 페이지별 인포메이션 조회 export const getCachedPageInformation = unstable_cache( - async (pageCode: string) => getPageInformation(pageCode), + async (pagePath: string) => getPageInformation(pagePath), ["page-information"], { tags: ["page-information"], @@ -180,34 +184,20 @@ export const getCachedPageInformation = unstable_cache( } ) -// 인포메이션 생성 -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) + + // 수정 가능한 필드만 허용 + const allowedFields = { + informationContent: updateData.informationContent, + attachmentFilePath: updateData.attachmentFilePath, + attachmentFileName: updateData.attachmentFileName, + updatedAt: new Date() + } + + const result = await updateInformation(id, allowedFields) if (!result) { return { @@ -233,56 +223,6 @@ export async function updateInformationData(input: UpdateInformationSchema) { } } -// 인포메이션 삭제 -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 { @@ -294,18 +234,18 @@ export async function getInformationDetail(id: number): Promise { +export async function checkInformationEditPermission(pagePath: string, userId: string): Promise { try { - // pageCode를 menuPath로 변환 (pageCode가 menuPath의 마지막 부분이라고 가정) - // 예: pageCode "vendor-list" -> menuPath "/evcp/vendor-list" 또는 "/partners/vendor-list" + // pagePath를 menuPath로 변환 (pagePath가 menuPath의 마지막 부분이라고 가정) + // 예: pagePath "vendor-list" -> menuPath "/evcp/vendor-list" 또는 "/partners/vendor-list" const menuPathQueries = [ - `/evcp/${pageCode}`, - `/partners/${pageCode}`, - `/${pageCode}`, // 루트 경로 - pageCode // 정확한 매칭 + `/evcp/${pagePath}`, + `/partners/${pagePath}`, + `/${pagePath}`, // 루트 경로 + pagePath // 정확한 매칭 ] - // menu_assignments에서 해당 pageCode와 매칭되는 메뉴 찾기 + // menu_assignments에서 해당 pagePath와 매칭되는 메뉴 찾기 const menuAssignment = await db .select() .from(menuAssignments) @@ -334,7 +274,7 @@ export async function checkInformationEditPermission(pageCode: string, userId: s // 캐시된 권한 확인 export const getCachedEditPermission = unstable_cache( - async (pageCode: string, userId: string) => checkInformationEditPermission(pageCode, userId), + async (pagePath: string, userId: string) => checkInformationEditPermission(pagePath, userId), ["information-edit-permission"], { tags: ["information-edit-permission"], diff --git a/lib/information/table/add-information-dialog.tsx b/lib/information/table/add-information-dialog.tsx deleted file mode 100644 index a879fbfe..00000000 --- a/lib/information/table/add-information-dialog.tsx +++ /dev/null @@ -1,329 +0,0 @@ -"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 ( - - - - 인포메이션 추가 - - 새로운 페이지 인포메이션을 추가합니다. - - - -
- -
- ( - - 페이지 코드 - - - - - - )} - /> - - ( - - 페이지명 - - - - - - )} - /> -
- - ( - - 제목 - - - - - - )} - /> - - ( - - 설명 - -