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/service.ts | 618 ++++++++++++++++++++++++--------------------- 1 file changed, 335 insertions(+), 283 deletions(-) (limited to 'lib/information/service.ts') 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 -- cgit v1.2.3