-
Notifications
You must be signed in to change notification settings - Fork 0
[feat] 커뮤니티 게시판 CRUD 및 에디터 기능 구현 #159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
50 commits
Select commit
Hold shift + click to select a range
26c48eb
[feat] 커뮤니티 게시판 라우팅 스캐폴딩
You-Hyuk 361ef28
[feat] 게시판 날짜 표기 정책 및 카테고리 상수 추가
You-Hyuk 2bcc34f
[feat] 커뮤니티 목록 페이지 구현
You-Hyuk 21b11f9
[feat] Tiptap 에디터 통합 및 entityMention 커스텀 노드 구현
You-Hyuk a49cf03
[feat] 슬래시 커맨드 멘션 검색 구현
You-Hyuk 4dfdf11
[feat] 게시글 상세 페이지 및 읽기 전용 렌더러 구현
You-Hyuk 03f122c
[feat] 게시글 작성 폼 완성 (제목·카테고리·entityTags 검증)
You-Hyuk 36b5891
[feat] 비로그인 상태의 추천 버튼 클릭 시 로그인 모달 노출
You-Hyuk 6b0d3e0
[feat] 목록/상세 페이지 실 API 연동
You-Hyuk 449c070
[feat] #157 게시글 추천/추천취소 API 실 연동
You-Hyuk aedc137
[feat] #157 멘션 검색(/) API 실 연동
You-Hyuk ad74a9c
[feat] #157 게시글 작성 API 실 연동
You-Hyuk fa9123f
[feat] #157 게시글 수정/삭제 API 실 연동
You-Hyuk 739229e
[fix] #157 본문이 비어있는 게시글 제출 시 명확한 안내 추가
You-Hyuk 62abe17
[fix] 게시글 카테고리 배지 색상을 디자인 시스템에 맞는 중립 톤으로 변경
You-Hyuk 6e9e83e
[fix] 게시글 카테고리 순서를 자유·정보·후기로 통일하고 "정보" 라벨로 변경
You-Hyuk 4b56abe
[chore] 글쓰기·등록 버튼을 공용 Button 컴포넌트로 통합
You-Hyuk 9f6d7c3
[fix] 게시글 작성 시 텍스트 필드에 남아있던 포커스 테두리 강조 제거
You-Hyuk 23fc980
[fix] 제목 입력창 포커스 시 모서리 반경이 줄어드는 문제 수정
You-Hyuk e4192a8
[feat] 게시글 에디터에 마크다운 서식·색상·링크 기능 추가
You-Hyuk 6822607
[fix] 후기·정보 카테고리 게시글 태그 필수 검증 제거
You-Hyuk 6deed1e
[feat] 게시글 목록 카드 UI 적용
You-Hyuk cb38d32
[feat] 커뮤니티 목록 페이지 인트로 배너 추가
You-Hyuk 75b1cdc
[feat] 커뮤니티 목록에 우측 사이드바 및 다가오는 공연 위젯 추가
You-Hyuk f842c06
[feat] 사이드바에 이번 주 인기글 위젯 추가 (mock)
You-Hyuk be0dd05
[feat] 사이드바에 인기 태그 위젯 추가 (mock)
You-Hyuk cebab11
[feat] 이번 주 인기글 위젯 API 연동
You-Hyuk ee865f5
[feat] 인기 태그 위젯 API 연동
You-Hyuk 6410a2f
[ui] 커뮤니티 페이지 레이아웃 타 페이지와 통일, 모바일 작성자명 버그 수정
You-Hyuk 9b66795
[ui] 커뮤니티 글쓰기 버튼을 게시글 목록 폭에 맞춰 정렬
You-Hyuk 2cf90df
[feat] 게시글 본문 글자수·태그 개수 제한 적용
You-Hyuk 976f8f2
[ui] 게시글 상세 카드 레이아웃 적용
You-Hyuk e5b9bd3
[ui] 게시글 상세 모바일 반응형 규칙 추가
You-Hyuk b7a02b2
[ui] 게시글 댓글 UI 컴포넌트 (목데이터)
You-Hyuk cf68fad
[feat] 게시글 댓글 API 연동 (목록·작성·삭제·좋아요)
You-Hyuk 89c7ea6
[ui] 게시글 상세 본문-추천 버튼 구분선 제거
You-Hyuk 18fb091
[fix] 텍스트 필드 포커스 시 의도치 않은 테두리 색상 변경 제거
You-Hyuk 31967e0
[fix] 댓글 작성 텍스트필드 리사이즈 비활성화
You-Hyuk 543e752
[feat] 댓글 글자수 제한(500자) 및 카운터 추가
You-Hyuk 50df3fe
[ui] 게시글 작성 페이지 상세 페이지와 레이아웃 정합
You-Hyuk cbfe1ab
[ui] 글쓰기 페이지 중첩된 박스 제거
You-Hyuk e7251c9
[ui] 게시글 작성 버튼/헤더 텍스트 통일
You-Hyuk 2883062
[ui] 댓글/답글 디자인 개선: 작성폼 상단 배치, 아바타 제거, 인라인 멘션
You-Hyuk 05a68c1
[ui] 댓글 하단 구분선 제거, 답글 좌측 구분선 복원
You-Hyuk ce75ce0
[refactor] 멘션 검색 API에 limit 파라미터 명시
You-Hyuk 0dbea0e
[ui] 게시글 작성/상세 화면 디자인 정합성 개선
You-Hyuk df501d1
[fix] 텍스트필드 클릭 시 focus-visible로 인한 모서리 round 변경 방지
You-Hyuk 1d1792b
[fix] 댓글 액션 실패 시 에러 안내 및 게시글 날짜 표기 미래 시각 오차 방지
You-Hyuk 95e49b2
[chore] npm audit high severity 취약점 해결 (js-yaml)
You-Hyuk e8042db
[fix] PR #159 CodeRabbit 리뷰 반영: 게시글 수정 권한, 댓글 중복/유실, 추천 레이스 컨디션 수정
You-Hyuk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { EditorContent, useEditor } from '@tiptap/react' | ||
| import StarterKit from '@tiptap/starter-kit' | ||
| import Color from '@tiptap/extension-color' | ||
| import { TextStyle } from '@tiptap/extension-text-style' | ||
|
|
||
| import { EntityMentionCard, EntityMentionChip } from './editor/entityMentionExtensions' | ||
| import styles from './PostContentView.module.css' | ||
|
|
||
| // 작성 시 저장된 Tiptap JSON을 읽기 전용 에디터로 그대로 다시 렌더링한다 — | ||
| // 멘션 카드 NodeView를 작성/열람 양쪽에서 동일하게 재사용하기 위함 | ||
| function PostContentView({ content }) { | ||
| const editor = useEditor({ | ||
| extensions: [StarterKit, TextStyle, Color, EntityMentionCard, EntityMentionChip], | ||
| content, | ||
| editable: false, | ||
| }) | ||
|
|
||
| if (!editor) return null | ||
|
|
||
| return ( | ||
| <div className={styles.prose}> | ||
| <EditorContent editor={editor} /> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| export default PostContentView |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| .prose { | ||
| max-width: 42rem; | ||
| font-size: 0.9375rem; | ||
| line-height: 1.8; | ||
| color: var(--color-text); | ||
| } | ||
|
|
||
| .prose :global(.ProseMirror) { | ||
| outline: none; | ||
| } | ||
|
|
||
| .prose :global(p) { | ||
| margin: 0 0 0.875rem; | ||
| } | ||
|
|
||
| .prose :global(p:last-child) { | ||
| margin-bottom: 0; | ||
| } | ||
|
|
||
| .prose :global(h1), | ||
| .prose :global(h2), | ||
| .prose :global(h3) { | ||
| margin: 1.5rem 0 0.75rem; | ||
| font-weight: 700; | ||
| line-height: 1.35; | ||
| color: var(--color-text); | ||
| } | ||
|
|
||
| .prose :global(h1:first-child), | ||
| .prose :global(h2:first-child), | ||
| .prose :global(h3:first-child) { | ||
| margin-top: 0; | ||
| } | ||
|
|
||
| .prose :global(h1) { | ||
| font-size: 1.5rem; | ||
| } | ||
|
|
||
| .prose :global(h2) { | ||
| font-size: 1.3125rem; | ||
| } | ||
|
|
||
| .prose :global(h3) { | ||
| font-size: 1.1875rem; | ||
| } | ||
|
|
||
| .prose :global(ul), | ||
| .prose :global(ol) { | ||
| margin: 0 0 0.875rem; | ||
| padding-left: 1.25rem; | ||
| } | ||
|
|
||
| .prose :global(li) { | ||
| margin: 0.3125rem 0; | ||
| } | ||
|
|
||
| .prose :global(li p) { | ||
| margin: 0; | ||
| } | ||
|
|
||
| .prose :global(blockquote) { | ||
| margin: 0 0 0.875rem; | ||
| padding-left: 0.875rem; | ||
| border-left: 3px solid var(--color-border); | ||
| color: var(--color-text-muted); | ||
| } | ||
|
|
||
| .prose :global(a) { | ||
| color: var(--color-accent); | ||
| text-decoration: underline; | ||
| text-underline-offset: 2px; | ||
| } | ||
|
|
||
| /* 엔티티 멘션 카드·칩은 NodeViewWrapper로 감싸진 자체 링크 스타일을 쓰므로 | ||
| 위 범용 링크 스타일(빨간 밑줄)이 덮어쓰지 않도록 분리한다 */ | ||
| .prose :global([data-node-view-wrapper] a) { | ||
| color: inherit; | ||
| text-decoration: none; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { Link } from 'react-router-dom' | ||
|
|
||
| import { ROUTES } from '@/constants/routes' | ||
| import { MENTION_TYPE_LABEL, POST_CATEGORY_LABEL } from '@/constants/post' | ||
| import { formatPostDate } from '@/utils/date' | ||
| import styles from './PostListItem.module.css' | ||
|
|
||
| const MAX_VISIBLE_TAGS = 2 | ||
|
|
||
| function PostListItem({ post }) { | ||
| const { id, category, title, authorNickname, recommendCount, viewCount, createdAt, entityTags } = post | ||
| const visibleTags = entityTags?.slice(0, MAX_VISIBLE_TAGS) ?? [] | ||
| const hiddenTagCount = (entityTags?.length ?? 0) - visibleTags.length | ||
|
|
||
| return ( | ||
| <Link to={ROUTES.COMMUNITY_DETAIL(id)} className={styles.row}> | ||
| <div className={styles.main}> | ||
| <span className={styles.badge}> | ||
| {POST_CATEGORY_LABEL[category] ?? category} | ||
| </span> | ||
| <span className={styles.title}>{title}</span> | ||
| </div> | ||
| <div className={styles.bottomRow}> | ||
| <div className={styles.meta}> | ||
| <span className={styles.author}>{authorNickname ?? '탈퇴 회원'}</span> | ||
| <span className={styles.dot} aria-hidden="true">·</span> | ||
| <span className={styles.date}>{formatPostDate(createdAt)}</span> | ||
| <span className={styles.dot} aria-hidden="true">·</span> | ||
| <span className={styles.stat}> | ||
| <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"> | ||
| <path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" /> | ||
| <circle cx="12" cy="12" r="3" /> | ||
| </svg> | ||
| {viewCount.toLocaleString()} | ||
| </span> | ||
| <span className={styles.stat}> | ||
| <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"> | ||
| <path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" /> | ||
| </svg> | ||
| {recommendCount.toLocaleString()} | ||
| </span> | ||
| </div> | ||
| {visibleTags.length > 0 && ( | ||
| <div className={styles.tags}> | ||
| <span className={styles.dot} aria-hidden="true">·</span> | ||
| {visibleTags.map((tag) => ( | ||
| <span key={`${tag.entityType}:${tag.entityId}`} className={styles.tagChip}> | ||
| {MENTION_TYPE_LABEL[tag.entityType]} · {tag.title} | ||
| </span> | ||
| ))} | ||
| {hiddenTagCount > 0 && <span className={styles.tagChip}>+{hiddenTagCount}</span>} | ||
| </div> | ||
| )} | ||
| </div> | ||
| </Link> | ||
| ) | ||
| } | ||
|
|
||
| export default PostListItem |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| .row { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 0.375rem; | ||
| padding: 1rem; | ||
| background: var(--color-surface); | ||
| border: 1px solid var(--color-border); | ||
| border-radius: var(--radius-lg); | ||
| color: inherit; | ||
| text-decoration: none; | ||
| transition: border-color 0.15s; | ||
| } | ||
|
|
||
| .row:hover { | ||
| border-color: var(--color-accent-border); | ||
| } | ||
|
|
||
| .main { | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 0.625rem; | ||
| min-width: 0; | ||
| } | ||
|
|
||
| .bottomRow { | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 0.5rem; | ||
| } | ||
|
|
||
| .tags { | ||
| display: flex; | ||
| align-items: center; | ||
| flex-wrap: nowrap; | ||
| gap: 0.375rem; | ||
| min-width: 0; | ||
| overflow: hidden; | ||
| } | ||
|
|
||
| .tagChip { | ||
| flex-shrink: 0; | ||
| max-width: 9.5rem; | ||
| padding: 0.125rem 0.5rem; | ||
| font-size: 0.6875rem; | ||
| color: var(--color-text-muted); | ||
| border: 1px solid var(--color-border); | ||
| border-radius: var(--radius-xs); | ||
| white-space: nowrap; | ||
| overflow: hidden; | ||
| text-overflow: ellipsis; | ||
| } | ||
|
|
||
| .badge { | ||
| flex-shrink: 0; | ||
| padding: 0.2em 0.55em; | ||
| font-size: 0.7rem; | ||
| font-weight: 700; | ||
| letter-spacing: 0.03em; | ||
| color: var(--color-text-muted); | ||
| background: var(--color-surface-hover); | ||
| border-radius: var(--radius-xs); | ||
| line-height: 1.4; | ||
| } | ||
|
|
||
| .title { | ||
| font-size: 0.9375rem; | ||
| font-weight: 500; | ||
| color: var(--color-text); | ||
| white-space: nowrap; | ||
| overflow: hidden; | ||
| text-overflow: ellipsis; | ||
| min-width: 0; | ||
| } | ||
|
|
||
| .meta { | ||
| display: flex; | ||
| align-items: center; | ||
| flex-wrap: wrap; | ||
| gap: 0.375rem; | ||
| flex-shrink: 0; | ||
| font-size: 0.8125rem; | ||
| color: var(--color-text-muted); | ||
| font-variant-numeric: tabular-nums; | ||
| } | ||
|
|
||
| .dot { | ||
| opacity: 0.6; | ||
| } | ||
|
|
||
| .stat { | ||
| display: inline-flex; | ||
| align-items: center; | ||
| gap: 0.25rem; | ||
| } | ||
|
|
||
| @media (max-width: 767px) { | ||
| .bottomRow { | ||
| flex-direction: column; | ||
| align-items: flex-start; | ||
| gap: 0.375rem; | ||
| } | ||
|
|
||
| .meta { | ||
| align-self: stretch; | ||
| } | ||
|
|
||
| .tags .dot { | ||
| display: none; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import styles from './PostListItemSkeleton.module.css' | ||
|
|
||
| function PostListItemSkeleton() { | ||
| return ( | ||
| <div className={styles.row}> | ||
| <div className={styles.main}> | ||
| <div className={styles.badge} /> | ||
| <div className={styles.title} /> | ||
| </div> | ||
| <div className={styles.meta} /> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| export default PostListItemSkeleton |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Restrict the edit form to the post author.
The public
/community/:idroute and the edit route both load the post withgetPost.PostDetailPageuses the response'sisAuthorfield, butPostWritePagerenders the form without checking it. An authenticated non-author can therefore load the edit form, even if the PATCH request is rejected by server-side ownership checks. CheckexistingPost.isAuthorbefore rendering and show an access-denied state or redirect.🤖 Prompt for AI Agents