From 795b4915069c44f500a91638e16ded67b9e16618 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 1 Jul 2025 11:46:33 +0000 Subject: (최겸) 정보시스템 공지사항 개발 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/notice/repository.ts | 244 ++++++++++++++++++++++++++++++++++ lib/notice/service.ts | 324 ++++++++++++++++++++++++++++++++++++++++++++++ lib/notice/validations.ts | 80 ++++++++++++ 3 files changed, 648 insertions(+) create mode 100644 lib/notice/repository.ts create mode 100644 lib/notice/service.ts create mode 100644 lib/notice/validations.ts (limited to 'lib') diff --git a/lib/notice/repository.ts b/lib/notice/repository.ts new file mode 100644 index 00000000..84e64f00 --- /dev/null +++ b/lib/notice/repository.ts @@ -0,0 +1,244 @@ +import { asc, desc, eq, ilike, and, count, sql } from "drizzle-orm" +import db from "@/db/db" +import { notice, users, type Notice, type NewNotice } from "@/db/schema" + +// 최신 패턴: 트랜잭션을 지원하는 공지사항 조회 +export async function selectNoticeLists( + tx: typeof db, + params: { + where?: ReturnType + orderBy?: (ReturnType | ReturnType)[] + offset?: number + limit?: number + } +) { + const { where, orderBy, offset = 0, limit = 10 } = params + + return tx + .select({ + id: notice.id, + pagePath: notice.pagePath, + title: notice.title, + content: notice.content, + authorId: notice.authorId, + isActive: notice.isActive, + createdAt: notice.createdAt, + updatedAt: notice.updatedAt, + authorName: users.name, + authorEmail: users.email, + }) + .from(notice) + .leftJoin(users, eq(notice.authorId, users.id)) + .where(where) + .orderBy(...(orderBy ?? [desc(notice.createdAt)])) + .offset(offset) + .limit(limit) +} + +// 최신 패턴: 트랜잭션을 지원하는 카운트 조회 +export async function countNoticeLists( + tx: typeof db, + where?: ReturnType +) { + const res = await tx + .select({ count: count() }) + .from(notice) + .where(where) + + return res[0]?.count ?? 0 +} + +// 기존 패턴 (하위 호환성을 위해 유지) +export async function selectNotice(input: { page: number; per_page: number; sort?: string; pagePath?: string; title?: string; authorId?: number; isActive?: boolean; from?: string; to?: string }) { + const { page, per_page = 50, sort, pagePath, title, authorId, isActive, from, to } = input + + const conditions = [] + + if (pagePath) { + conditions.push(ilike(notice.pagePath, `%${pagePath}%`)) + } + + if (title) { + conditions.push(ilike(notice.title, `%${title}%`)) + } + + if (authorId) { + conditions.push(eq(notice.authorId, authorId)) + } + + if (isActive !== null && isActive !== undefined) { + conditions.push(eq(notice.isActive, isActive)) + } + + if (from) { + conditions.push(sql`${notice.createdAt} >= ${from}`) + } + + if (to) { + conditions.push(sql`${notice.createdAt} <= ${to}`) + } + + const offset = (page - 1) * per_page + + // 정렬 설정 + let orderBy = desc(notice.createdAt); + + if (sort && Array.isArray(sort) && sort.length > 0) { + const sortItem = sort[0]; + if (sortItem.id === "createdAt") { + orderBy = sortItem.desc ? desc(notice.createdAt) : asc(notice.createdAt); + } + } + + const whereClause = conditions.length > 0 ? and(...conditions) : undefined + + const data = await db + .select({ + id: notice.id, + pagePath: notice.pagePath, + title: notice.title, + content: notice.content, + authorId: notice.authorId, + isActive: notice.isActive, + createdAt: notice.createdAt, + updatedAt: notice.updatedAt, + authorName: users.name, + authorEmail: users.email, + }) + .from(notice) + .leftJoin(users, eq(notice.authorId, users.id)) + .where(whereClause) + .orderBy(orderBy) + .limit(per_page) + .offset(offset) + + return data +} + +// 기존 패턴: 공지사항 총 개수 조회 +export async function countNotice(input: { pagePath?: string; title?: string; authorId?: number; isActive?: boolean; from?: string; to?: string }) { + const { pagePath, title, authorId, isActive, from, to } = input + + const conditions = [] + + if (pagePath) { + conditions.push(ilike(notice.pagePath, `%${pagePath}%`)) + } + + if (title) { + conditions.push(ilike(notice.title, `%${title}%`)) + } + + if (authorId) { + conditions.push(eq(notice.authorId, authorId)) + } + + if (isActive !== null && isActive !== undefined) { + conditions.push(eq(notice.isActive, isActive)) + } + + if (from) { + conditions.push(sql`${notice.createdAt} >= ${from}`) + } + + if (to) { + conditions.push(sql`${notice.createdAt} <= ${to}`) + } + + const whereClause = conditions.length > 0 ? and(...conditions) : undefined + + const result = await db + .select({ count: count() }) + .from(notice) + .where(whereClause) + + return result[0]?.count ?? 0 +} + +// 페이지 경로별 공지사항 조회 (활성화된 것만, 작성자 정보 포함) +export async function getNoticesByPagePath(pagePath: string): Promise> { + const result = await db + .select({ + id: notice.id, + pagePath: notice.pagePath, + title: notice.title, + content: notice.content, + authorId: notice.authorId, + isActive: notice.isActive, + createdAt: notice.createdAt, + updatedAt: notice.updatedAt, + authorName: users.name, + authorEmail: users.email, + }) + .from(notice) + .leftJoin(users, eq(notice.authorId, users.id)) + .where(and( + eq(notice.pagePath, pagePath), + eq(notice.isActive, true) + )) + .orderBy(desc(notice.createdAt)) + + return result +} + +// 공지사항 생성 +export async function insertNotice(data: NewNotice): Promise { + const result = await db + .insert(notice) + .values(data) + .returning() + + return result[0] +} + +// 공지사항 수정 +export async function updateNotice(id: number, data: Partial): Promise { + const result = await db + .update(notice) + .set({ ...data, updatedAt: new Date() }) + .where(eq(notice.id, id)) + .returning() + + return result[0] || null +} + +// 공지사항 삭제 +export async function deleteNoticeById(id: number): Promise { + const result = await db + .delete(notice) + .where(eq(notice.id, id)) + + return (result.rowCount ?? 0) > 0 +} + +// 공지사항 다중 삭제 +export async function deleteNoticeByIds(ids: number[]): Promise { + const result = await db + .delete(notice) + .where(sql`${notice.id} = ANY(${ids})`) + + return result.rowCount ?? 0 +} + +// ID로 공지사항 조회 (작성자 정보 포함) +export async function getNoticeById(id: number): Promise<(Notice & { authorName: string | null; authorEmail: string | null }) | null> { + const result = await db + .select({ + id: notice.id, + pagePath: notice.pagePath, + title: notice.title, + content: notice.content, + authorId: notice.authorId, + isActive: notice.isActive, + createdAt: notice.createdAt, + updatedAt: notice.updatedAt, + authorName: users.name, + authorEmail: users.email, + }) + .from(notice) + .leftJoin(users, eq(notice.authorId, users.id)) + .where(eq(notice.id, id)) + .limit(1) + + return result[0] || null +} \ No newline at end of file diff --git a/lib/notice/service.ts b/lib/notice/service.ts new file mode 100644 index 00000000..24b03fe9 --- /dev/null +++ b/lib/notice/service.ts @@ -0,0 +1,324 @@ +"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 { notice, pageInformation } from "@/db/schema" + +import type { + CreateNoticeSchema, + UpdateNoticeSchema, + GetNoticeSchema +} from "./validations" + +import { + selectNotice, + countNotice, + getNoticesByPagePath, + insertNotice, + updateNotice, + deleteNoticeById, + deleteNoticeByIds, + getNoticeById, + selectNoticeLists, + countNoticeLists +} from "./repository" + +import type { Notice } from "@/db/schema/notice" + +export async function getNoticeLists(input: GetNoticeSchema) { + return unstable_cache( + async () => { + try { + // 고급 검색 로직 + const { page, perPage, search, filters, joinOperator, pagePath, title, content, authorId, isActive } = input + + // 기본 검색 조건들 + const conditions = [] + + // 검색어가 있으면 여러 필드에서 검색 + if (search && search.trim()) { + const searchConditions = [ + ilike(notice.pagePath, `%${search}%`), + ilike(notice.title, `%${search}%`), + ilike(notice.content, `%${search}%`) + ] + conditions.push(or(...searchConditions)) + } + + // 개별 필드 조건들 + if (pagePath && pagePath.trim()) { + conditions.push(ilike(notice.pagePath, `%${pagePath}%`)) + } + + if (title && title.trim()) { + conditions.push(ilike(notice.title, `%${title}%`)) + } + + if (content && content.trim()) { + conditions.push(ilike(notice.content, `%${content}%`)) + } + + if (authorId !== null && authorId !== undefined) { + conditions.push(eq(notice.authorId, authorId)) + } + + if (isActive !== null && isActive !== undefined) { + conditions.push(eq(notice.isActive, isActive)) + } + // 고급 필터 처리 + if (filters && filters.length > 0) { + const advancedConditions = filters.map(() => + filterColumns({ + table: notice, + 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(notice.createdAt) : asc(notice.createdAt) + } else if (item.id === "updatedAt") { + return item.desc ? desc(notice.updatedAt) : asc(notice.updatedAt) + } else if (item.id === "pagePath") { + return item.desc ? desc(notice.pagePath) : asc(notice.pagePath) + } else if (item.id === "title") { + return item.desc ? desc(notice.title) : asc(notice.title) + } else if (item.id === "authorId") { + return item.desc ? desc(notice.authorId) : asc(notice.authorId) + } else if (item.id === "isActive") { + return item.desc ? desc(notice.isActive) : asc(notice.isActive) + } else { + return desc(notice.createdAt) // 기본값 + } + }) + : [desc(notice.createdAt)] + + // 트랜잭션 내부에서 Repository 호출 + const { data, total } = await db.transaction(async (tx) => { + const data = await selectNoticeLists(tx, { + where: finalWhere, + orderBy, + offset, + limit: input.perPage, + }) + + const total = await countNoticeLists(tx, finalWhere) + return { data, total } + }) + + const pageCount = Math.ceil(total / input.perPage) + + return { data, pageCount, total } + } catch (err) { + console.error("Failed to get notice lists:", err) + // 에러 발생 시 기본값 반환 + return { data: [], pageCount: 0, total: 0 } + } + }, + [JSON.stringify(input)], + { + revalidate: 3600, + tags: ["notice-lists"], + } + )() +} + +// 기존 패턴 (하위 호환성을 위해 유지) +export async function getNoticeList(input: Partial<{ page: number; per_page: number; sort?: string; pagePath?: string; title?: string; authorId?: number; isActive?: boolean; from?: string; to?: string }> & { page: number; per_page: number }) { + unstable_noStore() + + try { + const [data, total] = await Promise.all([ + selectNotice(input), + countNotice(input) + ]) + + const pageCount = Math.ceil(total / input.per_page) + + return { + data, + pageCount, + total + } + } catch (error) { + console.error("Failed to get notice list:", error) + throw new Error(getErrorMessage(error)) + } +} + +// 페이지별 공지사항 조회 (일반 사용자용) +export async function getPageNotices(pagePath: string): Promise> { + try { + return await getNoticesByPagePath(pagePath) + } catch (error) { + console.error(`Failed to get notices for page ${pagePath}:`, error) + return [] + } +} + +// 캐시된 페이지별 공지사항 조회 +export const getCachedPageNotices = unstable_cache( + async (pagePath: string) => getPageNotices(pagePath), + ["page-notices"], + { + tags: ["page-notices"], + revalidate: 3600, // 1시간 캐시 + } +) + +// 공지사항 생성 +export async function createNotice(input: CreateNoticeSchema) { + try { + const result = await insertNotice(input) + + revalidateTag("page-notices") + revalidateTag("notice-lists") + + return { + success: true, + data: result, + message: "공지사항이 성공적으로 생성되었습니다." + } + } catch (error) { + console.error("Failed to create notice:", error) + return { + success: false, + message: getErrorMessage(error) + } + } +} + +// 공지사항 수정 +export async function updateNoticeData(input: UpdateNoticeSchema) { + try { + const { id, ...updateData } = input + const result = await updateNotice(id, updateData) + + if (!result) { + return { + success: false, + message: "공지사항을 찾을 수 없거나 수정에 실패했습니다." + } + } + + revalidateTag("page-notices") + revalidateTag("notice-lists") + + return { + success: true, + message: "공지사항이 성공적으로 수정되었습니다." + } + } catch (error) { + console.error("Failed to update notice:", error) + return { + success: false, + message: getErrorMessage(error) + } + } +} + +// 공지사항 삭제 +export async function deleteNotice(id: number) { + try { + const success = await deleteNoticeById(id) + + if (!success) { + return { + success: false, + message: "공지사항을 찾을 수 없거나 삭제에 실패했습니다." + } + } + + revalidateTag("page-notices") + revalidateTag("notice-lists") + + return { + success: true, + message: "공지사항이 성공적으로 삭제되었습니다." + } + } catch (error) { + console.error("Failed to delete notice:", error) + return { + success: false, + message: getErrorMessage(error) + } + } +} + +// 공지사항 다중 삭제 +export async function deleteMultipleNotices(ids: number[]) { + try { + const deletedCount = await deleteNoticeByIds(ids) + + revalidateTag("page-notices") + revalidateTag("notice-lists") + + return { + success: true, + deletedCount, + message: `${deletedCount}개의 공지사항이 성공적으로 삭제되었습니다.` + } + } catch (error) { + console.error("Failed to delete multiple notices:", error) + return { + success: false, + message: getErrorMessage(error) + } + } +} + +// ID로 공지사항 조회 +export async function getNoticeDetail(id: number): Promise<(Notice & { authorName: string | null; authorEmail: string | null }) | null> { + try { + return await getNoticeById(id) + } catch (error) { + console.error(`Failed to get notice detail for id ${id}:`, error) + return null + } +} + +// pagePath 목록 조회 (정보 시스템에서 사용) +export async function getPagePathList(): Promise> { + try { + const result = await db + .selectDistinct({ + pagePath: pageInformation.pagePath, + pageName: pageInformation.pageName + }) + .from(pageInformation) + .where(eq(pageInformation.isActive, true)) + .orderBy(asc(pageInformation.pagePath)) + + return result.map(item => ({ + pagePath: item.pagePath, + pageName: item.pageName || item.pagePath + })) + } catch (error) { + console.error("Failed to get page path list:", error) + return [] + } +} \ No newline at end of file diff --git a/lib/notice/validations.ts b/lib/notice/validations.ts new file mode 100644 index 00000000..05e84af9 --- /dev/null +++ b/lib/notice/validations.ts @@ -0,0 +1,80 @@ +import { z } from "zod" +import { + createSearchParamsCache, + parseAsArrayOf, + parseAsInteger, + parseAsString, + parseAsStringEnum, + parseAsBoolean, +} from "nuqs/server" +import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers" +import { Notice } from "@/db/schema/notice" + +// 공지사항 생성 스키마 +export const createNoticeSchema = z.object({ + pagePath: z.string().min(1, "페이지 경로를 입력해주세요"), + title: z.string().min(1, "제목을 입력해주세요"), + content: z.string().min(1, "내용을 입력해주세요"), + authorId: z.number().min(1, "작성자를 선택해주세요"), + isActive: z.boolean().default(true), +}) + +// 공지사항 수정 스키마 +export const updateNoticeSchema = z.object({ + id: z.number(), + pagePath: z.string().min(1, "페이지 경로를 입력해주세요"), + title: z.string().min(1, "제목을 입력해주세요"), + content: z.string().min(1, "내용을 입력해주세요"), + isActive: z.boolean().default(true), +}) + +// 현대적인 검색 파라미터 캐시 +export const searchParamsNoticeCache = createSearchParamsCache({ + flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]), + page: parseAsInteger.withDefault(1), + perPage: parseAsInteger.withDefault(10), + sort: getSortingStateParser().withDefault([ + { id: "createdAt", desc: true }, + ]), + + // 기본 검색 필드들 + pagePath: parseAsString.withDefault(""), + title: parseAsString.withDefault(""), + content: parseAsString.withDefault(""), + authorId: parseAsInteger, + isActive: parseAsBoolean, + + // 고급 필터 + filters: getFiltersStateParser().withDefault([]), + joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), + search: parseAsString.withDefault(""), + + // 날짜 범위 + from: parseAsString.withDefault(""), + to: parseAsString.withDefault(""), +}) + +// 타입 추출 +export type CreateNoticeSchema = z.infer +export type UpdateNoticeSchema = z.infer +export type GetNoticeSchema = Awaited> + +// 기존 스키마 (하위 호환성을 위해 유지) +export const getNoticeSchema = z.object({ + page: z.coerce.number().default(1), + per_page: z.coerce.number().default(10), + sort: z.string().optional(), + pagePath: z.string().optional(), + title: z.string().optional(), + authorId: z.coerce.number().optional(), + isActive: z.coerce.boolean().optional(), + from: z.string().optional(), + to: z.string().optional(), +}) + +// 페이지 경로별 공지사항 조회 스키마 +export const getPageNoticeSchema = z.object({ + pagePath: z.string().min(1, "페이지 경로를 입력해주세요"), +}) + +export type GetPageNoticeSchema = z.infer \ No newline at end of file -- cgit v1.2.3 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 --- config/informationColumnsConfig.ts | 27 +- config/noticeColumnsConfig.ts | 54 ++++ 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 +- 11 files changed, 184 insertions(+), 1210 deletions(-) create mode 100644 config/noticeColumnsConfig.ts 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') diff --git a/config/informationColumnsConfig.ts b/config/informationColumnsConfig.ts index 6357cfa3..508cb846 100644 --- a/config/informationColumnsConfig.ts +++ b/config/informationColumnsConfig.ts @@ -10,9 +10,9 @@ export interface InformationColumnConfig { export const informationColumnsConfig: InformationColumnConfig[] = [ { - id: "pageCode", - label: "페이지 코드", - excelHeader: "페이지 코드", + id: "pagePath", + label: "페이지 경로", + excelHeader: "페이지 경로", }, { id: "pageName", @@ -20,24 +20,9 @@ export const informationColumnsConfig: InformationColumnConfig[] = [ excelHeader: "페이지명", }, { - id: "title", - label: "제목", - excelHeader: "제목", - }, - { - id: "description", - label: "설명", - excelHeader: "설명", - }, - { - id: "noticeTitle", - label: "공지사항 제목", - excelHeader: "공지사항 제목", - }, - { - id: "noticeContent", - label: "공지사항 내용", - excelHeader: "공지사항 내용", + id: "informationContent", + label: "내용", + excelHeader: "내용", }, { id: "attachmentFileName", diff --git a/config/noticeColumnsConfig.ts b/config/noticeColumnsConfig.ts new file mode 100644 index 00000000..9e9565fb --- /dev/null +++ b/config/noticeColumnsConfig.ts @@ -0,0 +1,54 @@ +import { Notice } from "@/db/schema/notice" + +export interface NoticeColumnConfig { + id: keyof Notice + label: string + group?: string + excelHeader?: string + type?: string +} + +export const noticeColumnsConfig: NoticeColumnConfig[] = [ + { + id: "id", + label: "ID", + excelHeader: "ID", + }, + { + id: "pagePath", + label: "페이지 경로", + excelHeader: "페이지 경로", + }, + { + id: "title", + label: "제목", + excelHeader: "제목", + }, + { + id: "content", + label: "내용", + excelHeader: "내용", + }, + { + id: "authorId", + label: "작성자 ID", + excelHeader: "작성자 ID", + }, + { + 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/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 ( - - - - 인포메이션 추가 - - 새로운 페이지 인포메이션을 추가합니다. - - - -
- -
- ( - - 페이지 코드 - - - - - - )} - /> - - ( - - 페이지명 - - - - - - )} - /> -
- - ( - - 제목 - - - - - - )} - /> - - ( - - 설명 - -