Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion src/app/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ import { RequireAuth } from '@/app/guards/RequireAuth';
import { AuthCallbackPage } from '@/pages/auth/AuthCallbackPage';
import { LoginPage } from '@/pages/auth/LoginPage';
import { SignupPage } from '@/pages/auth/SignupPage';
import { FamilyJoinPage } from '@/pages/family/FamilyJoinPage';
import { BoardCreatePage } from '@/pages/community/BoardCreatePage';
import { BoardDetailPage } from '@/pages/community/BoardDetailPage';
import { BoardEditPage } from '@/pages/community/BoardEditPage';
import { CommunityMyActivityPage } from '@/pages/community/CommunityMyActivityPage';
import { CommunityPage } from '@/pages/community/CommunityPage';
import { FamilyJoinPage } from '@/pages/family/FamilyJoinPage';
import { MainPage } from '@/pages/main/MainPage';
import { PetDetailPage } from '@/pages/my/PetDetailPage';
import { PetEditPage } from '@/pages/my/PetEditPage';
Expand All @@ -26,6 +30,38 @@ export const router = createBrowserRouter([
{ path: '/', element: <MainPage /> },
{ path: '/walk', element: <WalkPage /> },
{ path: '/community', element: <CommunityPage /> },
{
path: '/community/my',
element: (
<RequireAuth>
<CommunityMyActivityPage />
</RequireAuth>
),
},
{
path: '/community/new',
element: (
<RequireAuth>
<BoardCreatePage />
</RequireAuth>
),
},
{
path: '/community/:boardId',
element: (
<RequireAuth>
<BoardDetailPage />
</RequireAuth>
),
},
{
path: '/community/:boardId/edit',
element: (
<RequireAuth>
<BoardEditPage />
</RequireAuth>
),
},
{
path: '/my',
element: (
Expand Down
52 changes: 52 additions & 0 deletions src/features/community/api/boards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { apiClient } from '@/shared/api/axios';

import type {
BoardDetailResponse,
BoardListResponse,
CreateBoardRequest,
CreateBoardResponse,
DeleteBoardResponse,
TempSavedBoardResponse,
TempSaveBoardRequest,
TempSaveBoardResponse,
UpdateBoardRequest,
UpdateBoardResponse,
} from '../model/types';

export async function getBoardList(params: { page: number; size: number }): Promise<BoardListResponse> {
const response = await apiClient.get<BoardListResponse>('/boards', { params });
return response.data;
}

export async function createBoard(payload: CreateBoardRequest): Promise<CreateBoardResponse> {
const response = await apiClient.post<CreateBoardResponse>('/boards', payload);
return response.data;
}

export async function tempSaveBoard(payload: TempSaveBoardRequest): Promise<TempSaveBoardResponse> {
const { boardId, ...body } = payload;
const response = await apiClient.post<TempSaveBoardResponse>('/boards/temp-save', body, {
params: boardId !== undefined ? { boardId } : undefined,
});
return response.data;
}

export async function getTempSavedBoard(sessionKey: string): Promise<TempSavedBoardResponse> {
const response = await apiClient.get<TempSavedBoardResponse>(`/boards/temp-save/${sessionKey}`);
return response.data;
}

export async function getBoardDetail(boardId: number): Promise<BoardDetailResponse> {
const response = await apiClient.get<BoardDetailResponse>(`/boards/${boardId}`);
return response.data;
}

export async function updateBoard(boardId: number, payload: UpdateBoardRequest): Promise<UpdateBoardResponse> {
const response = await apiClient.patch<UpdateBoardResponse>(`/boards/${boardId}`, payload);
return response.data;
}

export async function deleteBoard(boardId: number): Promise<DeleteBoardResponse> {
const response = await apiClient.delete<DeleteBoardResponse>(`/boards/${boardId}`);
return response.data;
}
53 changes: 53 additions & 0 deletions src/features/community/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
export {
createBoard,
deleteBoard,
getBoardDetail,
getBoardList,
getTempSavedBoard,
tempSaveBoard,
updateBoard,
} from './api/boards';
export {
BOARD_DETAIL_STATUS_MESSAGES,
BOARD_DRAFT_STATUS_MESSAGES,
BOARD_LIST_STATUS_MESSAGES,
BOARD_MUTATION_STATUS_MESSAGES,
COMMUNITY_DRAFT_SESSION_KEY,
} from './lib/constants';
export {
clearStoredBoardDraftSessionKey,
getStoredBoardDraftSessionKey,
setStoredBoardDraftSessionKey,
} from './lib/storage';
export { INITIAL_BOARD_EDITOR_FORM_STATE, validateBoardEditorForm } from './lib/validation';
export { useBoardDetail } from './model/useBoardDetail';
export { useBoardEditorForm } from './model/useBoardEditorForm';
export { useBoardList } from './model/useBoardList';
export { useCreateBoard } from './model/useCreateBoard';
export { useDeleteBoard } from './model/useDeleteBoard';
export { useTempSaveBoard } from './model/useTempSaveBoard';
export { useTempSavedBoard } from './model/useTempSavedBoard';
export { useUpdateBoard } from './model/useUpdateBoard';
export { CommunityFeedCard } from './ui/CommunityFeedCard';
export { CommunityLayout } from './ui/CommunityLayout';
export { CommunityProfileCard } from './ui/CommunityProfileCard';
export { CommunitySidebarPanel } from './ui/CommunitySidebarPanel';
export { BoardDetailContent } from './ui/BoardDetailContent';
export { BoardEditorForm } from './ui/BoardEditorForm';
export { DeleteBoardDialog } from './ui/DeleteBoardDialog';
export type {
BoardDetailResponse,
BoardListItem,
BoardListResponse,
BoardPayload,
CreateBoardRequest,
CreateBoardResponse,
DeleteBoardResponse,
TempSavedBoardResponse,
TempSaveBoardRequest,
TempSaveBoardResponse,
UpdateBoardRequest,
UpdateBoardResponse,
} from './model/types';
export type { BoardEditorFormErrors, BoardEditorFormState } from './lib/validation';
export type { BoardEditorMode, UseBoardEditorFormOptions } from './model/useBoardEditorForm';
30 changes: 30 additions & 0 deletions src/features/community/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export const COMMUNITY_DRAFT_SESSION_KEY = 'community-board-draft-session-key';

export const BOARD_LIST_STATUS_MESSAGES: Partial<Record<number, string>> = {
400: '잘못된 요청입니다.',
401: '로그인이 필요합니다. 다시 로그인해주세요.',
500: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
};

export const BOARD_MUTATION_STATUS_MESSAGES: Partial<Record<number, string>> = {
400: '입력값을 다시 확인해주세요.',
401: '로그인이 필요합니다. 다시 로그인해주세요.',
403: '게시글을 처리할 권한이 없습니다.',
500: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
};

export const BOARD_DETAIL_STATUS_MESSAGES: Partial<Record<number, string>> = {
400: '잘못된 게시글 요청입니다.',
401: '로그인이 필요합니다. 다시 로그인해주세요.',
403: '게시글을 조회할 권한이 없습니다.',
404: '게시글을 찾을 수 없습니다.',
500: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
};

export const BOARD_DRAFT_STATUS_MESSAGES: Partial<Record<number, string>> = {
400: '임시 저장 요청을 처리할 수 없습니다.',
401: '로그인이 필요합니다. 다시 로그인해주세요.',
403: '임시 저장 게시글을 처리할 권한이 없습니다.',
404: '임시 저장된 게시글을 찾을 수 없습니다.',
500: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
};
26 changes: 26 additions & 0 deletions src/features/community/lib/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { COMMUNITY_DRAFT_SESSION_KEY } from './constants';

export function getStoredBoardDraftSessionKey(): string | null {
if (typeof window === 'undefined') {
return null;
}

const value = window.localStorage.getItem(COMMUNITY_DRAFT_SESSION_KEY);
return value?.trim() ? value : null;
}

export function setStoredBoardDraftSessionKey(sessionKey: string) {
if (typeof window === 'undefined') {
return;
}

window.localStorage.setItem(COMMUNITY_DRAFT_SESSION_KEY, sessionKey);
}

export function clearStoredBoardDraftSessionKey() {
if (typeof window === 'undefined') {
return;
}

window.localStorage.removeItem(COMMUNITY_DRAFT_SESSION_KEY);
}
30 changes: 30 additions & 0 deletions src/features/community/lib/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export interface BoardEditorFormState {
boardTitle: string;
boardContent: string;
imageFileUrls: string[];
}

export interface BoardEditorFormErrors {
boardTitle?: string;
boardContent?: string;
}

export const INITIAL_BOARD_EDITOR_FORM_STATE: BoardEditorFormState = {
boardTitle: '',
boardContent: '',
imageFileUrls: [],
};

export function validateBoardEditorForm(form: BoardEditorFormState): BoardEditorFormErrors {
const errors: BoardEditorFormErrors = {};

if (!form.boardTitle.trim()) {
errors.boardTitle = '게시글 제목을 입력해주세요.';
}

if (!form.boardContent.trim()) {
errors.boardContent = '게시글 내용을 입력해주세요.';
}

return errors;
}
77 changes: 77 additions & 0 deletions src/features/community/model/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
export interface BoardListItem {
boardId: number;
boardTitle: string;
boardContentPreview: string;
thumbnailImageUrl: string | null;
nickname: string;
viewCount: number;
commentCount: number;
likeCount: number;
dislikeCount: number;
createdAt: string;
modifiedAt: string;
}

export interface BoardListResponse {
message: string;
boards: BoardListItem[];
}

export interface BoardPayload {
boardTitle: string;
boardContent: string;
imageFileUrls: string[];
}

export type CreateBoardRequest = BoardPayload;

export interface CreateBoardResponse {
message: string;
boardId: number;
}

export interface TempSaveBoardRequest {
boardId?: number;
boardTitle?: string;
boardContent?: string;
imageFileUrl?: string;
imageFileUrls?: string[];
}

export interface TempSaveBoardResponse {
message: string;
sessionKey: string;
}

export interface TempSavedBoardResponse {
message: string;
boardTitle: string;
boardContent: string;
imageFileUrl: string | null;
imageFileUrls?: string[] | null;
}

export interface BoardDetailResponse {
message: string;
boardId: number;
boardTitle: string;
boardContent: string;
imageFileUrls: string[];
profileUrl: string | null;
nickname: string;
likeCount?: number;
commentCount?: number;
viewCount: number;
boardCreatedAt: string;
modifiedAt: string;
}

export type UpdateBoardRequest = BoardPayload;

export interface UpdateBoardResponse {
message: string;
}

export interface DeleteBoardResponse {
message: string;
}
14 changes: 14 additions & 0 deletions src/features/community/model/useBoardDetail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useQuery } from '@tanstack/react-query';

import { getBoardDetail } from '@/features/community/api/boards';
import { queryKeys } from '@/shared/lib/react-query/queryKey';

export function useBoardDetail(boardId: number | null) {
const isValidId = boardId !== null && !Number.isNaN(boardId);

return useQuery({
queryKey: isValidId ? queryKeys.boards.detail(boardId) : ['boards', 'detail', 'idle'],
queryFn: () => getBoardDetail(boardId as number),
enabled: isValidId,
});
}
Loading
Loading