summaryrefslogtreecommitdiff
path: root/lib/information/service.ts
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-01 02:53:18 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-01 02:53:18 +0000
commitd66d308169e559457878c02e3b0443da22693241 (patch)
tree257b4d1d3345d2828e6ea8473938b5113d2ae733 /lib/information/service.ts
parent4ab3f7fd98f544244df972f3f333bbe3525d54f8 (diff)
(최겸) 정보시스템 인포메이션 기능 개발
Diffstat (limited to 'lib/information/service.ts')
-rw-r--r--lib/information/service.ts343
1 files changed, 343 insertions, 0 deletions
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<GetInformationSchema> & { page: number; per_page: number }) {
+ unstable_noStore()
+
+ try {
+ const [data, total] = await Promise.all([
+ selectInformation(input as Parameters<typeof selectInformation>[0]),
+ countInformation(input as Parameters<typeof countInformation>[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<PageInformation | null> {
+ 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<PageInformation | null> {
+ 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<boolean> {
+ 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