feat(WEB): 동적 게시판, 파트너 관리, 공지 팝업 모달 추가
- 동적 게시판 시스템 구현 (/boards/[boardCode]) - 파트너 관리 페이지 및 폼 추가 - 공지 팝업 모달 컴포넌트 (NoticePopupModal) - localStorage 기반 1일간 숨김 기능 - 테스트 페이지 (/test/popup) - IntegratedListTemplateV2 개선 - 기타 버그 수정 및 타입 개선 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { ClipboardList, ArrowLeft, Save } from 'lucide-react';
|
||||
import type { Board, BoardFormData, BoardTarget, BoardStatus } from './types';
|
||||
import { BOARD_TARGETS, BOARD_STATUS_LABELS } from './types';
|
||||
@@ -29,6 +30,14 @@ const MOCK_DEPARTMENTS = [
|
||||
{ id: 5, name: '마케팅팀' },
|
||||
];
|
||||
|
||||
// TODO: API에서 권한 목록 가져오기
|
||||
const MOCK_PERMISSIONS = [
|
||||
{ code: 'admin', name: '관리자' },
|
||||
{ code: 'manager', name: '매니저' },
|
||||
{ code: 'staff', name: '직원' },
|
||||
{ code: 'guest', name: '게스트' },
|
||||
];
|
||||
|
||||
interface BoardFormProps {
|
||||
mode: 'create' | 'edit';
|
||||
board?: Board;
|
||||
@@ -56,6 +65,7 @@ export function BoardForm({ mode, board, onSubmit }: BoardFormProps) {
|
||||
const [formData, setFormData] = useState<BoardFormData>({
|
||||
target: 'all',
|
||||
targetName: '',
|
||||
permissions: [],
|
||||
boardName: '',
|
||||
status: 'active',
|
||||
});
|
||||
@@ -66,6 +76,7 @@ export function BoardForm({ mode, board, onSubmit }: BoardFormProps) {
|
||||
setFormData({
|
||||
target: board.target,
|
||||
targetName: board.targetName || '',
|
||||
permissions: board.permissions || [],
|
||||
boardName: board.boardName,
|
||||
status: board.status,
|
||||
});
|
||||
@@ -89,7 +100,18 @@ export function BoardForm({ mode, board, onSubmit }: BoardFormProps) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
target: value,
|
||||
targetName: value === 'all' ? '' : prev.targetName,
|
||||
targetName: value === 'department' ? prev.targetName : '',
|
||||
permissions: value === 'permission' ? prev.permissions : [],
|
||||
}));
|
||||
};
|
||||
|
||||
// 권한 체크박스 핸들러
|
||||
const handlePermissionChange = (code: string, checked: boolean) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
permissions: checked
|
||||
? [...(prev.permissions || []), code]
|
||||
: (prev.permissions || []).filter(p => p !== code),
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -151,6 +173,25 @@ export function BoardForm({ mode, board, onSubmit }: BoardFormProps) {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
{formData.target === 'permission' && (
|
||||
<div className="flex-1 flex flex-wrap gap-4 items-center border rounded-md px-3 py-2">
|
||||
{MOCK_PERMISSIONS.map((perm) => (
|
||||
<div key={perm.code} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`perm-${perm.code}`}
|
||||
checked={(formData.permissions || []).includes(perm.code)}
|
||||
onCheckedChange={(checked) => handlePermissionChange(perm.code, checked as boolean)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`perm-${perm.code}`}
|
||||
className="font-normal cursor-pointer text-sm"
|
||||
>
|
||||
{perm.name}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ interface ApiResponse<T> {
|
||||
function transformApiToFrontend(apiData: BoardApiData): Board {
|
||||
const extraSettings = apiData.extra_settings || {};
|
||||
|
||||
// permissions 추출 (read 권한 기준으로 사용)
|
||||
const permissions = extraSettings.permissions?.read || [];
|
||||
|
||||
return {
|
||||
id: String(apiData.id),
|
||||
boardCode: apiData.board_code,
|
||||
@@ -35,6 +38,7 @@ function transformApiToFrontend(apiData: BoardApiData): Board {
|
||||
target: extraSettings.target || 'all',
|
||||
targetId: extraSettings.target_id,
|
||||
targetName: extraSettings.target_name,
|
||||
permissions: permissions.length > 0 ? permissions : undefined,
|
||||
boardName: apiData.name,
|
||||
description: apiData.description || undefined,
|
||||
status: apiData.is_active ? 'active' : 'inactive',
|
||||
@@ -50,14 +54,26 @@ function transformApiToFrontend(apiData: BoardApiData): Board {
|
||||
* 프론트엔드 데이터 → API 요청 형식 변환
|
||||
*/
|
||||
function transformFrontendToApi(data: BoardFormData & { boardCode?: string; description?: string }, isUpdate = false): Record<string, unknown> {
|
||||
// extra_settings 구성
|
||||
const extraSettings: Record<string, unknown> = {
|
||||
target: data.target,
|
||||
target_name: data.target === 'department' ? data.targetName : null,
|
||||
};
|
||||
|
||||
// 권한 대상인 경우 permissions 추가
|
||||
if (data.target === 'permission' && data.permissions && data.permissions.length > 0) {
|
||||
extraSettings.permissions = {
|
||||
read: data.permissions,
|
||||
write: data.permissions,
|
||||
manage: data.permissions,
|
||||
};
|
||||
}
|
||||
|
||||
const result: Record<string, unknown> = {
|
||||
name: data.boardName,
|
||||
description: data.description || null,
|
||||
is_active: data.status === 'active',
|
||||
extra_settings: {
|
||||
target: data.target,
|
||||
target_name: data.target === 'department' ? data.targetName : null,
|
||||
},
|
||||
extra_settings: extraSettings,
|
||||
};
|
||||
|
||||
// 생성 시에만 board_code 전송 (수정 시에는 코드 변경 불가)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// 게시판 상태 타입
|
||||
export type BoardStatus = 'active' | 'inactive';
|
||||
|
||||
// 대상 타입 (전사, 부서 등)
|
||||
export type BoardTarget = 'all' | 'department';
|
||||
// 대상 타입 (전사, 부서, 권한)
|
||||
export type BoardTarget = 'all' | 'department' | 'permission';
|
||||
|
||||
// 게시판 타입 (프론트엔드용)
|
||||
export interface Board {
|
||||
@@ -12,6 +12,7 @@ export interface Board {
|
||||
target: BoardTarget;
|
||||
targetId?: number;
|
||||
targetName?: string; // 부서명 (target이 department일 때)
|
||||
permissions?: string[]; // 권한 코드 목록 (target이 permission일 때)
|
||||
boardName: string;
|
||||
description?: string;
|
||||
status: BoardStatus;
|
||||
@@ -69,6 +70,7 @@ export interface BoardApiData {
|
||||
export interface BoardFormData {
|
||||
target: BoardTarget;
|
||||
targetName?: string;
|
||||
permissions?: string[]; // 권한 코드 목록 (target이 permission일 때)
|
||||
boardName: string;
|
||||
status: BoardStatus;
|
||||
}
|
||||
@@ -89,10 +91,12 @@ export const BOARD_STATUS_COLORS: Record<BoardStatus, string> = {
|
||||
export const BOARD_TARGET_LABELS: Record<BoardTarget, string> = {
|
||||
all: '전사',
|
||||
department: '부서',
|
||||
permission: '권한',
|
||||
};
|
||||
|
||||
// 대상 옵션
|
||||
export const BOARD_TARGETS = [
|
||||
{ value: 'all' as BoardTarget, label: '전사' },
|
||||
{ value: 'department' as BoardTarget, label: '부서' },
|
||||
{ value: 'permission' as BoardTarget, label: '권한' },
|
||||
];
|
||||
|
||||
371
src/components/board/DynamicBoard/actions.ts
Normal file
371
src/components/board/DynamicBoard/actions.ts
Normal file
@@ -0,0 +1,371 @@
|
||||
/**
|
||||
* 동적 게시판 Server Actions
|
||||
* 일반 게시판 게시글 API 호출 (/api/v1/boards/{code}/posts)
|
||||
*/
|
||||
|
||||
'use server';
|
||||
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type {
|
||||
PostApiData,
|
||||
PostPaginationResponse,
|
||||
ApiResponse,
|
||||
PostFilters,
|
||||
CommentApiData,
|
||||
CommentsApiResponse,
|
||||
} from '@/components/customer-center/shared/types';
|
||||
|
||||
/**
|
||||
* 게시글 목록 조회
|
||||
*/
|
||||
export async function getDynamicBoardPosts(
|
||||
boardCode: string,
|
||||
filters?: PostFilters
|
||||
): Promise<{ success: boolean; data?: PostPaginationResponse; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.is_notice !== undefined) params.append('is_notice', String(filters.is_notice));
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
if (filters?.per_page) params.append('per_page', String(filters.per_page));
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시글 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
let result: ApiResponse<PostPaginationResponse>;
|
||||
try {
|
||||
result = await response.json();
|
||||
} catch {
|
||||
console.error('[DynamicBoardActions] JSON parse error');
|
||||
return { success: false, error: '서버 응답 형식 오류입니다.' };
|
||||
}
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '게시글 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
console.error('[DynamicBoardActions] getDynamicBoardPosts error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 상세 조회
|
||||
*/
|
||||
export async function getDynamicBoardPost(
|
||||
boardCode: string,
|
||||
postId: number | string
|
||||
): Promise<{ success: boolean; data?: PostApiData; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시글 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PostApiData> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success || !result.data) {
|
||||
return { success: false, error: result.message || '게시글을 찾을 수 없습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
console.error('[DynamicBoardActions] getDynamicBoardPost error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 등록
|
||||
*/
|
||||
export async function createDynamicBoardPost(
|
||||
boardCode: string,
|
||||
data: {
|
||||
title: string;
|
||||
content: string;
|
||||
is_secret?: boolean;
|
||||
is_notice?: boolean;
|
||||
custom_fields?: Record<string, string>;
|
||||
}
|
||||
): Promise<{ success: boolean; data?: PostApiData; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시글 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '게시글 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
console.error('[DynamicBoardActions] createDynamicBoardPost error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 수정
|
||||
*/
|
||||
export async function updateDynamicBoardPost(
|
||||
boardCode: string,
|
||||
postId: number | string,
|
||||
data: {
|
||||
title?: string;
|
||||
content?: string;
|
||||
is_secret?: boolean;
|
||||
is_notice?: boolean;
|
||||
custom_fields?: Record<string, string>;
|
||||
}
|
||||
): Promise<{ success: boolean; data?: PostApiData; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시글 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '게시글 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
console.error('[DynamicBoardActions] updateDynamicBoardPost error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 삭제
|
||||
*/
|
||||
export async function deleteDynamicBoardPost(
|
||||
boardCode: string,
|
||||
postId: number | string
|
||||
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시글 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '게시글 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[DynamicBoardActions] deleteDynamicBoardPost error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 댓글 API =====
|
||||
|
||||
/**
|
||||
* 댓글 목록 조회
|
||||
*/
|
||||
export async function getDynamicBoardComments(
|
||||
boardCode: string,
|
||||
postId: number | string
|
||||
): Promise<{ success: boolean; data?: CommentsApiResponse; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '댓글 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<CommentsApiResponse> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '댓글 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
console.error('[DynamicBoardActions] getDynamicBoardComments error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 댓글 작성
|
||||
*/
|
||||
export async function createDynamicBoardComment(
|
||||
boardCode: string,
|
||||
postId: number | string,
|
||||
content: string
|
||||
): Promise<{ success: boolean; data?: CommentApiData; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content }),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '댓글 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '댓글 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
console.error('[DynamicBoardActions] createDynamicBoardComment error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 댓글 수정
|
||||
*/
|
||||
export async function updateDynamicBoardComment(
|
||||
boardCode: string,
|
||||
postId: number | string,
|
||||
commentId: number | string,
|
||||
content: string
|
||||
): Promise<{ success: boolean; data?: CommentApiData; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments/${commentId}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ content }),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '댓글 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '댓글 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
console.error('[DynamicBoardActions] updateDynamicBoardComment error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 댓글 삭제
|
||||
*/
|
||||
export async function deleteDynamicBoardComment(
|
||||
boardCode: string,
|
||||
postId: number | string,
|
||||
commentId: number | string
|
||||
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments/${commentId}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '댓글 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '댓글 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[DynamicBoardActions] deleteDynamicBoardComment error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user