summaryrefslogtreecommitdiff
path: root/lib/notice
diff options
context:
space:
mode:
Diffstat (limited to 'lib/notice')
-rw-r--r--lib/notice/repository.ts23
-rw-r--r--lib/notice/service.ts35
-rw-r--r--lib/notice/storage-utils.ts213
-rw-r--r--lib/notice/validations.ts44
4 files changed, 311 insertions, 4 deletions
diff --git a/lib/notice/repository.ts b/lib/notice/repository.ts
index fb941ac9..897eb5b0 100644
--- a/lib/notice/repository.ts
+++ b/lib/notice/repository.ts
@@ -2,8 +2,10 @@ import { desc, eq, and, sql } from "drizzle-orm"
import db from "@/db/db"
import { notice, users, type Notice, type NewNotice } from "@/db/schema"
-// 페이지 경로별 공지사항 조회 (활성화된 것만, 작성자 정보 포함)
+// 페이지 경로별 공지사항 조회 (활성화된 것만, 작성자 정보 포함, 유효기간 내 공지사항만)
export async function getNoticesByPagePath(pagePath: string): Promise<Array<Notice & { authorName: string | null; authorEmail: string | null }>> {
+ const currentTime = new Date()
+
const result = await db
.select({
id: notice.id,
@@ -12,6 +14,10 @@ export async function getNoticesByPagePath(pagePath: string): Promise<Array<Noti
content: notice.content,
authorId: notice.authorId,
isActive: notice.isActive,
+ isPopup: notice.isPopup,
+ startAt: notice.startAt,
+ endAt: notice.endAt,
+ dontShowDuration: notice.dontShowDuration,
createdAt: notice.createdAt,
updatedAt: notice.updatedAt,
authorName: users.name,
@@ -21,7 +27,10 @@ export async function getNoticesByPagePath(pagePath: string): Promise<Array<Noti
.leftJoin(users, eq(notice.authorId, users.id))
.where(and(
eq(notice.pagePath, pagePath),
- eq(notice.isActive, true)
+ eq(notice.isActive, true),
+ // // 유효기간 필터링: startAt과 endAt이 모두 null이거나 현재 시간이 범위 내에 있어야 함
+ // sql`(${notice.startAt} IS NULL OR ${notice.startAt} <= ${currentTime})`,
+ // sql`(${notice.endAt} IS NULL OR ${notice.endAt} >= ${currentTime})`
))
.orderBy(desc(notice.createdAt))
@@ -32,7 +41,11 @@ export async function getNoticesByPagePath(pagePath: string): Promise<Array<Noti
export async function insertNotice(data: NewNotice): Promise<Notice> {
const result = await db
.insert(notice)
- .values(data)
+ .values({
+ ...data,
+ createdAt: new Date(),
+ updatedAt: new Date()
+ })
.returning()
return result[0]
@@ -77,6 +90,10 @@ export async function getNoticeById(id: number): Promise<(Notice & { authorName:
content: notice.content,
authorId: notice.authorId,
isActive: notice.isActive,
+ isPopup: notice.isPopup,
+ startAt: notice.startAt,
+ endAt: notice.endAt,
+ dontShowDuration: notice.dontShowDuration,
createdAt: notice.createdAt,
updatedAt: notice.updatedAt,
authorName: users.name,
diff --git a/lib/notice/service.ts b/lib/notice/service.ts
index 9c05b98f..12f2ed2e 100644
--- a/lib/notice/service.ts
+++ b/lib/notice/service.ts
@@ -21,6 +21,23 @@ import {
import type { Notice } from "@/db/schema/notice"
+// 페이지별 공지사항 조회 (강제 모달용)
+export async function getPageNoticesForModal(pagePath: string): Promise<Array<Notice & { authorName: string | null; authorEmail: string | null }>> {
+ try {
+ console.log('🔍 Notice Service - 모달용 조회 시작:', { pagePath })
+ const result = await getNoticesByPagePath(pagePath)
+ console.log('📊 Notice Service - 모달용 조회 결과:', {
+ pagePath,
+ noticesCount: result.length,
+ notices: result.map(n => ({ id: n.id, title: n.title, pagePath: n.pagePath }))
+ })
+ return result
+ } catch (error) {
+ console.error(`Failed to get notices for modal on page ${pagePath}:`, error)
+ return []
+ }
+}
+
// 간단한 공지사항 목록 조회 (페이지네이션 없이 전체 조회)
export async function getNoticeLists(): Promise<{ data: Array<Notice & { authorName: string | null; authorEmail: string | null }> }> {
try {
@@ -33,6 +50,10 @@ export async function getNoticeLists(): Promise<{ data: Array<Notice & { authorN
content: notice.content,
authorId: notice.authorId,
isActive: notice.isActive,
+ isPopup: notice.isPopup,
+ startAt: notice.startAt,
+ endAt: notice.endAt,
+ dontShowDuration: notice.dontShowDuration,
createdAt: notice.createdAt,
updatedAt: notice.updatedAt,
authorName: users.name,
@@ -163,7 +184,19 @@ export async function deleteMultipleNotices(ids: number[]) {
// ID로 공지사항 조회
export async function getNoticeDetail(id: number): Promise<(Notice & { authorName: string | null; authorEmail: string | null }) | null> {
try {
- return await getNoticeById(id)
+ const result = await getNoticeById(id)
+ // 유효기간 검증 추가 (현재 시간이 유효기간 내에 있는지 확인)
+ if (result) {
+ const currentTime = new Date()
+ const isValid = (!result.startAt || result.startAt <= currentTime) &&
+ (!result.endAt || result.endAt >= currentTime)
+
+ if (!isValid) {
+ console.log(`Notice ${id} is not in valid time range`)
+ return null
+ }
+ }
+ return result
} catch (error) {
console.error(`Failed to get notice detail for id ${id}:`, error)
return null
diff --git a/lib/notice/storage-utils.ts b/lib/notice/storage-utils.ts
new file mode 100644
index 00000000..41a512d8
--- /dev/null
+++ b/lib/notice/storage-utils.ts
@@ -0,0 +1,213 @@
+/**
+ * 공지사항 로컬 스토리지 유틸리티 함수들
+ * '다시 보지 않기' 기능을 위한 저장 및 확인 로직을 제공합니다.
+ */
+
+export interface NoticeStorageOptions {
+ noticeId: number
+ duration: 'day' | 'never'
+}
+
+/**
+ * 공지사항 '다시 보지 않기' 설정을 로컬 스토리지에 저장합니다.
+ * @param options - 공지사항 ID와 기간 설정
+ */
+export function setNoticeDontShow(options: NoticeStorageOptions): void {
+ const { noticeId, duration } = options
+
+ if (typeof window === 'undefined') {
+ console.warn('setNoticeDontShow: window 객체가 없습니다. 클라이언트 사이드에서만 사용 가능합니다.')
+ return
+ }
+
+ try {
+ const key = `notice-${noticeId}-${duration}`
+ let value: string
+
+ if (duration === 'day') {
+ // 오늘 자정까지의 타임스탬프 계산
+ const today = new Date()
+ const tomorrow = new Date(today)
+ tomorrow.setDate(tomorrow.getDate() + 1)
+ tomorrow.setHours(0, 0, 0, 0) // 자정으로 설정
+
+ value = tomorrow.getTime().toString()
+ } else if (duration === 'never') {
+ // 영구적으로 저장 (매우 먼 미래의 날짜)
+ const neverDate = new Date('2099-12-31T23:59:59.999Z')
+ value = neverDate.getTime().toString()
+ } else {
+ throw new Error(`지원하지 않는 기간 설정입니다: ${duration}`)
+ }
+
+ localStorage.setItem(key, value)
+ console.log(`공지사항 ${noticeId}의 '다시 보지 않기' 설정을 저장했습니다:`, { key, value, duration })
+ } catch (error) {
+ console.error('공지사항 설정 저장 중 오류 발생:', error)
+ }
+}
+
+/**
+ * 공지사항의 '다시 보지 않기' 설정이 유효한지 확인합니다.
+ * @param options - 공지사항 ID와 기간 설정
+ * @returns 설정이 유효하면 true, 만료되었거나 설정되지 않았으면 false
+ */
+export function isNoticeDontShowValid(options: NoticeStorageOptions): boolean {
+ const { noticeId, duration } = options
+
+ if (typeof window === 'undefined') {
+ console.warn('isNoticeDontShowValid: window 객체가 없습니다. 클라이언트 사이드에서만 사용 가능합니다.')
+ return false
+ }
+
+ try {
+ const key = `notice-${noticeId}-${duration}`
+ const storedValue = localStorage.getItem(key)
+
+ if (!storedValue) {
+ return false // 설정이 없음
+ }
+
+ const expirationTime = parseInt(storedValue, 10)
+ const currentTime = Date.now()
+
+ if (isNaN(expirationTime)) {
+ console.warn(`잘못된 만료 시간 값입니다: ${storedValue}`)
+ localStorage.removeItem(key) // 잘못된 값은 삭제
+ return false
+ }
+
+ const isValid = currentTime < expirationTime
+
+ if (!isValid) {
+ // 만료된 설정은 자동으로 삭제
+ localStorage.removeItem(key)
+ console.log(`공지사항 ${noticeId}의 만료된 설정을 삭제했습니다.`)
+ }
+
+ return isValid
+ } catch (error) {
+ console.error('공지사항 설정 확인 중 오류 발생:', error)
+ return false
+ }
+}
+
+/**
+ * 특정 공지사항의 모든 '다시 보지 않기' 설정을 초기화합니다.
+ * @param noticeId - 공지사항 ID
+ */
+export function clearNoticeDontShowSettings(noticeId: number): void {
+ if (typeof window === 'undefined') {
+ console.warn('clearNoticeDontShowSettings: window 객체가 없습니다. 클라이언트 사이드에서만 사용 가능합니다.')
+ return
+ }
+
+ try {
+ const keysToRemove: string[] = []
+
+ // 모든 가능한 키 패턴을 확인
+ for (let i = 0; i < localStorage.length; i++) {
+ const key = localStorage.key(i)
+ if (key && key.startsWith(`notice-${noticeId}-`)) {
+ keysToRemove.push(key)
+ }
+ }
+
+ // 모든 관련 키 삭제
+ keysToRemove.forEach(key => localStorage.removeItem(key))
+
+ if (keysToRemove.length > 0) {
+ console.log(`공지사항 ${noticeId}의 모든 '다시 보지 않기' 설정을 초기화했습니다:`, keysToRemove)
+ }
+ } catch (error) {
+ console.error('공지사항 설정 초기화 중 오류 발생:', error)
+ }
+}
+
+/**
+ * 모든 공지사항의 '다시 보지 않기' 설정을 초기화합니다.
+ * 주의: 이 함수는 모든 공지사항 설정을 삭제하므로 신중하게 사용해야 합니다.
+ */
+export function clearAllNoticeDontShowSettings(): void {
+ if (typeof window === 'undefined') {
+ console.warn('clearAllNoticeDontShowSettings: window 객체가 없습니다. 클라이언트 사이드에서만 사용 가능합니다.')
+ return
+ }
+
+ try {
+ const keysToRemove: string[] = []
+
+ // 모든 가능한 키 패턴을 확인
+ for (let i = 0; i < localStorage.length; i++) {
+ const key = localStorage.key(i)
+ if (key && key.startsWith('notice-')) {
+ keysToRemove.push(key)
+ }
+ }
+
+ // 모든 관련 키 삭제
+ keysToRemove.forEach(key => localStorage.removeItem(key))
+
+ if (keysToRemove.length > 0) {
+ console.log(`모든 공지사항의 '다시 보지 않기' 설정을 초기화했습니다: ${keysToRemove.length}개`)
+ }
+ } catch (error) {
+ console.error('모든 공지사항 설정 초기화 중 오류 발생:', error)
+ }
+}
+
+/**
+ * 현재 로컬 스토리지에서 공지사항과 관련된 모든 키를 조회합니다.
+ * 디버깅 목적으로 사용됩니다.
+ */
+export function getAllNoticeStorageKeys(): string[] {
+ if (typeof window === 'undefined') {
+ console.warn('getAllNoticeStorageKeys: window 객체가 없습니다. 클라이언트 사이드에서만 사용 가능합니다.')
+ return []
+ }
+
+ try {
+ const keys: string[] = []
+
+ for (let i = 0; i < localStorage.length; i++) {
+ const key = localStorage.key(i)
+ if (key && key.startsWith('notice-')) {
+ keys.push(key)
+ }
+ }
+
+ return keys
+ } catch (error) {
+ console.error('공지사항 키 조회 중 오류 발생:', error)
+ return []
+ }
+}
+
+/**
+ * 특정 공지사항의 현재 설정 상태를 조회합니다.
+ * 디버깅 목적으로 사용됩니다.
+ */
+export function getNoticeStorageInfo(noticeId: number): Array<{ key: string; value: string | null; duration: string }> {
+ if (typeof window === 'undefined') {
+ console.warn('getNoticeStorageInfo: window 객체가 없습니다. 클라이언트 사이드에서만 사용 가능합니다.')
+ return []
+ }
+
+ try {
+ const info: Array<{ key: string; value: string | null; duration: string }> = []
+
+ for (let i = 0; i < localStorage.length; i++) {
+ const key = localStorage.key(i)
+ if (key && key.startsWith(`notice-${noticeId}-`)) {
+ const value = localStorage.getItem(key)
+ const duration = key.split('-').pop() || 'unknown'
+ info.push({ key, value, duration })
+ }
+ }
+
+ return info
+ } catch (error) {
+ console.error('공지사항 정보 조회 중 오류 발생:', error)
+ return []
+ }
+}
diff --git a/lib/notice/validations.ts b/lib/notice/validations.ts
index 146f8e09..6fca1bea 100644
--- a/lib/notice/validations.ts
+++ b/lib/notice/validations.ts
@@ -7,6 +7,28 @@ export const createNoticeSchema = z.object({
content: z.string().min(1, "내용을 입력해주세요"),
authorId: z.number().min(1, "작성자를 선택해주세요"),
isActive: z.boolean().default(true),
+ isPopup: z.boolean().default(false),
+ startAt: z.date().optional(),
+ endAt: z.date().optional(),
+ dontShowDuration: z.string().transform((val) => val as 'day' | 'never').optional().default('never'),
+}).refine((data) => {
+ // 팝업인 경우 시작일시와 종료일시는 필수
+ if (data.isPopup) {
+ return data.startAt && data.endAt;
+ }
+ return true;
+}, {
+ message: "팝업 공지사항은 시작일시와 종료일시를 모두 설정해야 합니다.",
+ path: ["startAt"],
+}).refine((data) => {
+ // 팝업인 경우에만 시작일시와 종료일시가 모두 설정된 경우 종료일이 시작일보다 과거인지 확인
+ if (data.isPopup && data.startAt && data.endAt) {
+ return data.endAt >= data.startAt;
+ }
+ return true;
+}, {
+ message: "종료일시는 시작일시보다 과거일 수 없습니다.",
+ path: ["endAt"],
})
// 공지사항 수정 스키마
@@ -16,6 +38,28 @@ export const updateNoticeSchema = z.object({
title: z.string().min(1, "제목을 입력해주세요"),
content: z.string().min(1, "내용을 입력해주세요"),
isActive: z.boolean().default(true),
+ isPopup: z.boolean().default(false),
+ startAt: z.date().optional(),
+ endAt: z.date().optional(),
+ dontShowDuration: z.string().transform((val) => val as 'day' | 'never').optional().default('never'),
+}).refine((data) => {
+ // 팝업인 경우 시작일시와 종료일시는 필수
+ if (data.isPopup) {
+ return data.startAt && data.endAt;
+ }
+ return true;
+}, {
+ message: "팝업 공지사항은 시작일시와 종료일시를 모두 설정해야 합니다.",
+ path: ["startAt"],
+}).refine((data) => {
+ // 팝업인 경우에만 시작일시와 종료일시가 모두 설정된 경우 종료일이 시작일보다 과거인지 확인
+ if (data.isPopup && data.startAt && data.endAt) {
+ return data.endAt >= data.startAt;
+ }
+ return true;
+}, {
+ message: "종료일시는 시작일시보다 과거일 수 없습니다.",
+ path: ["endAt"],
})
// 페이지 경로별 공지사항 조회 스키마