Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
deb0eb0
feat: 게시글 작성 API 연결
nabbang6 Apr 3, 2026
4b289b0
fix: 게시글 작성 관련 버그 수정
nabbang6 Apr 3, 2026
81598b6
feat: 게시글 목록/상세 콘텐츠 렌더링 개선
nabbang6 Apr 3, 2026
2b1e913
feat: 게시글 수정 API 연결
nabbang6 Apr 5, 2026
4303ae2
feat: 게시글 삭제 API 연결
nabbang6 Apr 5, 2026
09cf581
refactor: PostCardContent를 ListContent / DetailContent로 분리
nabbang6 Apr 5, 2026
dc723f5
refactor: 게시글 수정 페이지 헤더를 CategorySelector로 통일
nabbang6 Apr 5, 2026
714c71b
fix: ActionMenu 클릭 시 Link 네비게이션되는 문제 수정
nabbang6 Apr 6, 2026
9c1d74a
feat: 게시글 작성/수정 페이지 이탈 방지 guard 추가
nabbang6 Apr 6, 2026
2ae1bc0
fix: 게시글 상세 페이지 네비게이션 개선
nabbang6 Apr 6, 2026
997c016
style: 에디터 첨부 영역 레이아웃 개선
nabbang6 Apr 6, 2026
8717a94
Merge branch 'develop' of https://github.com/Team-Weeth/weeth-client …
nabbang6 Apr 6, 2026
9e95ac5
fix: prettier 포맷팅 수정
nabbang6 Apr 6, 2026
db30143
fix: 게시글 작성/수정 후 목록 캐시 갱신 및 성공 토스트 추가
nabbang6 Apr 6, 2026
1a67d5e
feat: 게시판 글쓰기 버튼에 기수 미입력 검증 추가
nabbang6 Apr 6, 2026
8beeab9
fix: fileSize와 contentType을 FileAttachment에서 매핑하게 수정
nabbang6 Apr 6, 2026
283fad9
fix: 게시글 수정 시 기존 서버 파일을 제외하고 새로 추가된 파일만 포함하게 수정
nabbang6 Apr 6, 2026
1b67286
refactor: 헤더에서 작성/수정 중 버튼 분리
nabbang6 Apr 7, 2026
4cc519d
refactor: validatePost 유틸로 공통 검증 로직 분리
nabbang6 Apr 7, 2026
f72541b
fix: 코드 스플리팅 오버헤드 수정
nabbang6 Apr 7, 2026
faa08e0
refactor: 내부 서브 컴포넌트 및 useLineClamp 훅을 별도 파일로 분리
nabbang6 Apr 7, 2026
ef211b5
fix: contentType 빈 문자열 처리 누락 문제 수정
nabbang6 Apr 7, 2026
aab38b0
fix: 게시글 수정 시 hasChanges 오탐 가능성 처리
nabbang6 Apr 7, 2026
dcf94ef
comment: 의도적인 마운트 전용 effect의 ESLint 경고 처리
nabbang6 Apr 7, 2026
78846af
refactor: 렌더 중 store 변경을 useEffect + ref guard 패턴으로 변경
nabbang6 Apr 7, 2026
dbc5814
refactor: PostCardContent 컴포넌트 분리 처리
nabbang6 Apr 7, 2026
a442b8f
fix: ActionMenu 영역을 Button의 onClickCapture로 전파를 막도록 변경
nabbang6 Apr 7, 2026
caf69d5
refactor: TitleInput 컴포넌트 분리
nabbang6 Apr 7, 2026
cb09526
fix: 게시글 수정 페이지에 유효하지 않은 ID 에러 처리 추가
nabbang6 Apr 7, 2026
bbd7776
fix: 수정 페이지에서 변경 없이 뒤로가기 시 경고 다이얼로그 표시되는 문제 수정
nabbang6 Apr 7, 2026
db3d446
fix: 프로필 상태 로딩 중 글쓰기 버튼 클릭 시 잘못된 모달 표시되는 문제 수정
nabbang6 Apr 7, 2026
8ae77f6
refactor: isHtmlEmpty를 shared에서 board 유틸로 이동
nabbang6 Apr 7, 2026
8c23d62
Merge branch 'develop' of https://github.com/Team-Weeth/weeth-client …
nabbang6 Apr 8, 2026
7849f11
fix: 게시글 목록 조회 시 이미지 카드 불러올 수 있게 수정
nabbang6 Apr 8, 2026
0f5e0e7
chore: 불필요한 코드 주석 삭제
nabbang6 Apr 8, 2026
d2b643c
feat: 게시글 목록이 null일 때 표시되는 문구 추가
nabbang6 Apr 8, 2026
bb6140d
fix: board 페이지에서 드롭다운 메뉴 클릭 시 기능이 수행되지 않는 문제 수정
nabbang6 Apr 8, 2026
901a847
fix: prettier 포맷팅 수정
nabbang6 Apr 8, 2026
7e358e0
style: 게시글 상세 조회 시 답글 간 간격 추가
nabbang6 Apr 8, 2026
74e8f3d
fix: UI 표시와 스토어 상태 간 불일치 문제 수정
nabbang6 Apr 8, 2026
cc5cbf7
fix: ALL/유효하지 않은 게시판 ID가 board에 그대로 남는 문제 수정
nabbang6 Apr 8, 2026
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
14 changes: 13 additions & 1 deletion src/app/(private)/(main)/board/(with-nav)/BoardNavClient.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { useEffect } from 'react';
import { usePathname, useRouter } from 'next/navigation';

import { BoardNav } from '@/components/board';
import { useActiveBoardId, useSetActiveBoardId } from '@/stores/useBoardNavStore';
Expand All @@ -13,6 +14,10 @@ interface BoardNavClientProps {
function BoardNavClient({ items }: BoardNavClientProps) {
const activeBoardId = useActiveBoardId();
const setActiveBoardId = useSetActiveBoardId();
const pathname = usePathname();
const router = useRouter();

const isDetailPage = /^\/board\/\d+$/.test(pathname);

useEffect(() => {
if (activeBoardId === null) return;
Expand All @@ -22,7 +27,14 @@ function BoardNavClient({ items }: BoardNavClientProps) {
}
}, [items, activeBoardId, setActiveBoardId]);

return <BoardNav items={items} activeId={activeBoardId} onItemSelect={setActiveBoardId} />;
const handleItemSelect = (id: number | null) => {
setActiveBoardId(id);
if (isDetailPage) {
router.push('/board');
}
};

return <BoardNav items={items} activeId={activeBoardId} onItemSelect={handleItemSelect} />;
}

export { BoardNavClient };
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use client';

import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Divider } from '@/components/ui';
import {
PostCard,
Expand All @@ -11,6 +13,7 @@
} from '@/components/board';
import { formatShortDateTime } from '@/lib/formatTime';
import { toDisplayFile, isImageFileByType, mapComment } from '@/lib/board';
import { useSetActiveBoardId } from '@/stores/useBoardNavStore';
import { useUserId } from '@/stores/useUserStore';
import type { PostDetail } from '@/types/board';
import type { UploadFileItem } from '@/stores/usePostStore';
Expand All @@ -20,7 +23,13 @@
}

function PostDetailContent({ post }: PostDetailContentProps) {
const router = useRouter();
const currentUserId = useUserId();
const setActiveBoardId = useSetActiveBoardId();

useEffect(() => {
setActiveBoardId(post.boardId);
}, [post.boardId, setActiveBoardId]);

const isPostAuthor = currentUserId !== null && post.author.id === currentUserId;
const imageFiles = post.fileUrls
Expand All @@ -30,7 +39,7 @@
.filter((f) => !isImageFileByType(f.contentType))
.map(toDisplayFile);

const handleCommentSubmit = (_value: string, _file: UploadFileItem | null) => {

Check warning on line 42 in src/app/(private)/(main)/board/(with-nav)/[id]/PostDetailContent.tsx

View workflow job for this annotation

GitHub Actions / Lint & Build

'_file' is defined but never used

Check warning on line 42 in src/app/(private)/(main)/board/(with-nav)/[id]/PostDetailContent.tsx

View workflow job for this annotation

GitHub Actions / Lint & Build

'_value' is defined but never used
// TODO: 댓글 작성 API 연동
};

Expand All @@ -48,15 +57,16 @@
date={formatShortDateTime(post.time)}
hasAttachment={post.fileUrls.length > 0}
/>
{isPostAuthor && <PostActionMenu />}
{isPostAuthor && (
<PostActionMenu
postId={post.id}
onEdit={() => router.push(`/board/edit/${post.id}`)}
onDeleted={() => router.push('/board')}
/>
)}
</PostCard.Header>

<PostCard.Content
title={post.title}
content={post.content}
expandable={false}
variant="detail"
/>
<PostCard.DetailContent title={post.title} content={post.content} isNew={post.isNew} />

<PostCard.Images files={imageFiles} />

Expand Down
41 changes: 41 additions & 0 deletions src/app/(private)/(main)/board/edit/[id]/EditClientEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client';

import { useEffect, useRef } from 'react';

import { CategorySelector, PostEditorShell } from '@/components/board';
import { useBoardList } from '@/hooks';
import { toBoardNavItem } from '@/lib/board';
import { usePostStore } from '@/stores/usePostStore';
import type { PostDetail } from '@/types/board';

interface EditClientEditorProps {
post: PostDetail;
}

function EditClientEditor({ post }: EditClientEditorProps) {
const initializedRef = useRef(false);

useEffect(() => {
if (initializedRef.current) return;
initializedRef.current = true;
usePostStore.getState().initFromDetail(post);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

렌더중 store 변경은 권장되지 않으니까,, seEffect + ref guard 패턴이 더 좋을 거 같습니다!

}, [post]);

const { data: boards } = useBoardList();
const items = boards?.map(toBoardNavItem) ?? [];

const board = usePostStore((s) => s.board);
const setBoard = usePostStore((s) => s.setBoard);

const activeId = board ?? post.boardId;

return (
<PostEditorShell
align="center"
initialContent={post.content}
header={<CategorySelector items={items} activeId={activeId} onItemSelect={setBoard} />}
/>
);
}

export { EditClientEditor };
30 changes: 30 additions & 0 deletions src/app/(private)/(main)/board/edit/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { notFound } from 'next/navigation';

import { boardServerApi } from '@/lib/apis/board.server';
import { EditClientEditor } from './EditClientEditor';

interface PostEditPageProps {
params: Promise<{ id: string }>;
}

export default async function PostEditPage({ params }: PostEditPageProps) {
const { id } = await params;
const postId = Number(id);

if (Number.isNaN(postId)) {
notFound();
}

// TODO: 추후 하드코딩된 clubId 제거 예정
const response = await boardServerApi.getPostById('YUNJcjFKMO', postId).catch(() => null);

if (!response?.data) {
notFound();
}

return (
<main className="w-full">
<EditClientEditor post={response.data} />
</main>
);
}
49 changes: 29 additions & 20 deletions src/app/(private)/(main)/board/write/ClientEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,50 @@
'use client';

import dynamic from 'next/dynamic';
import { useEffect } from 'react';
import { useEffect, useLayoutEffect } from 'react';

import { TitleInput, CategorySelector } from '@/components/board';
import { CategorySelector, PostEditorShell } from '@/components/board';
import { useBoardList } from '@/hooks';
import { toBoardNavItem } from '@/lib/board';
import { usePostStore } from '@/stores/usePostStore';

const Editor = dynamic(() => import('@/components/board/Editor'), { ssr: false });
import { useActiveBoardId } from '@/stores/useBoardNavStore';

export default function ClientEditor() {
const { data: boards } = useBoardList();
const items = boards?.map(toBoardNavItem) ?? [];
const writableItems = items.filter((item) => item.type !== 'ALL');
const activeBoardId = useActiveBoardId();

const board = usePostStore((s) => s.board);
const setBoard = usePostStore((s) => s.setBoard);
const reset = usePostStore((s) => s.reset);

const defaultId = items.find((item) => item.type === 'ALL')?.id ?? items[0]?.id ?? null;
const isWritable = (id: number | null) =>
id !== null && writableItems.some((item) => item.id === id);

useEffect(() => {
if (board === null && defaultId !== null) {
setBoard(defaultId);
useLayoutEffect(() => {
reset();
if (isWritable(activeBoardId)) {
setBoard(activeBoardId);
}
}, [board, defaultId, setBoard]);
// eslint-disable-next-line react-hooks/exhaustive-deps -- 마운트 시 1회만 실행
}, []);

const activeId = isWritable(board)
? board
: isWritable(activeBoardId)
? activeBoardId
: (writableItems[0]?.id ?? null);

const activeId = board ?? defaultId;
useEffect(() => {
if (board !== activeId && activeId !== null) {
setBoard(activeId);
}
}, [board, activeId, setBoard]);

return (
<div className="mx-auto flex max-w-[1200px] flex-1 flex-col items-center gap-400 p-450">
<CategorySelector items={items} activeId={activeId} onItemSelect={setBoard} />
<div className="flex w-full flex-col items-start">
<TitleInput />
<div className="flex w-full items-center gap-200 rounded-lg p-100">
<Editor />
</div>
</div>
</div>
<PostEditorShell
align="center"
header={<CategorySelector items={items} activeId={activeId} onItemSelect={setBoard} />}
/>
);
}
20 changes: 12 additions & 8 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -523,11 +523,11 @@ select:focus {
}

.ProseMirror {
@apply text-text-strong min-h-[400px] focus:outline-none;
@apply text-text-strong focus:outline-none;
}

.ProseMirror > * {
@apply my-100;
.ProseMirror:not(.prose-readonly) {
@apply flex-1;
}

/* placeholder */
Expand All @@ -541,23 +541,23 @@ select:focus {
}

.ProseMirror h1 {
@apply typo-h3 mt-300;
@apply typo-h3 mt-300 mb-100;
}

.ProseMirror h2 {
@apply typo-sub1 mt-300;
@apply typo-sub1 mt-300 mb-100;
}

.ProseMirror h3 {
@apply typo-sub2 mt-300;
@apply typo-sub2 mt-300 mb-100;
}

.ProseMirror ul {
@apply text-text-alternative my-0 list-disc py-0 pl-500;
@apply text-text-alternative my-0 list-disc py-100 pl-500;
}

.ProseMirror ol {
@apply text-text-alternative my-0 list-decimal py-0 pl-500;
@apply text-text-alternative my-0 list-decimal py-100 pl-500;
}

.ProseMirror ul li,
Expand Down Expand Up @@ -625,6 +625,10 @@ select:focus {
}

/* 인라인 코드 */
.ProseMirror p:has(> code:not(pre code)) {
@apply py-100;
}

.ProseMirror code:not(pre code) {
background-color: var(--container-code);
color: var(--text-normal);
Expand Down
64 changes: 64 additions & 0 deletions src/components/board/ActionMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use client';

import { MoreVerticalIcon } from '@/assets/icons';
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Icon,
} from '@/components/ui';
import { cn } from '@/lib/cn';
import type { ButtonProps } from '@/components/ui';

interface ActionMenuProps {
className?: string;
onEdit?: () => void;
onDeleteSelect?: (event: Event) => void;
triggerVariant?: ButtonProps['variant'];
triggerSize?: ButtonProps['size'];
triggerClassName?: string;
}

/**
* 수정/삭제 드롭다운 메뉴
*
* 외부 핸들러에 동작을 위임합니다. post 삭제처럼 확인 다이얼로그 + API 호출이
* 내장된 동작이 필요하면 `PostActionMenu`를 사용하세요.
*/
function ActionMenu({
className,
onEdit,
onDeleteSelect,
triggerVariant = 'tertiary',
triggerSize = 'icon-md',
triggerClassName,
}: ActionMenuProps) {
return (
<DropdownMenu modal>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant={triggerVariant}
size={triggerSize}
className={cn('h-600 w-600', triggerClassName, className)}
aria-label="더보기"
onClickCapture={(e) => e.stopPropagation()}
>
<Icon src={MoreVerticalIcon} size={16} className="text-icon-normal" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[144px]">
<DropdownMenuItem onSelect={onEdit}>수정</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem destructive onSelect={onDeleteSelect}>
삭제
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

export { ActionMenu, type ActionMenuProps };
Loading
Loading