feat: 새 글 작성 페이지 구현#56
Conversation
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
|
""" Walkthrough이 변경사항은 게시글 생성 기능을 도입하며, 게시글 작성 페이지와 관련된 여러 UI 컴포넌트(책 선택, 본문 입력, 사진 업로드, 공개 범위 토글, 태그 선택 등)와 스타일 파일을 추가합니다. 또한, 책 검색 바텀시트의 탭별 필터링 로직을 리팩토링하고, 라우터에 게시글 작성 경로를 등록합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CreatePostPage
participant BookSearchBottomSheet
participant PhotoSection
participant TagSelectionSection
participant PrivacyToggleSection
User->>CreatePostPage: 접속/이동
User->>CreatePostPage: 책 선택 버튼 클릭
CreatePostPage->>BookSearchBottomSheet: 바텀시트 오픈
User->>BookSearchBottomSheet: 책 검색/탭 전환/선택
BookSearchBottomSheet->>CreatePostPage: 책 선택 결과 전달
User->>PhotoSection: 사진 추가/삭제
User->>TagSelectionSection: 태그 선택/해제
User->>PrivacyToggleSection: 공개/비공개 토글
User->>CreatePostPage: 본문 입력
User->>CreatePostPage: 완료 버튼 클릭
CreatePostPage->>CreatePostPage: 유효성 검사 및 데이터 수집
CreatePostPage->>User: 완료 처리(예: 네비게이션)
Estimated code review effort3 (~50분) Possibly related PRs
Suggested reviewers
Poem
""" 📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 6
🔭 Outside diff range comments (1)
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx (1)
89-165: 필터링 로직의 중복을 제거하세요.
useEffect와handleTabChange에서 동일한 필터링 로직이 반복됩니다. 공통 함수로 추출하여 중복을 제거하세요.+const filterBooks = (books: Book[], query: string) => { + if (query.trim() === '') { + return books; + } + return books.filter( + book => + book.title.toLowerCase().includes(query.toLowerCase()) || + book.author.toLowerCase().includes(query.toLowerCase()), + ); +}; useEffect(() => { const currentTabBooks = activeTab === 'saved' ? mockSavedBooks : mockGroupBooks; - if (searchQuery.trim() === '') { - setFilteredBooks(currentTabBooks); - } else { - const filtered = currentTabBooks.filter( - book => - book.title.toLowerCase().includes(searchQuery.toLowerCase()) || - book.author.toLowerCase().includes(searchQuery.toLowerCase()), - ); - setFilteredBooks(filtered); - } + setFilteredBooks(filterBooks(currentTabBooks, searchQuery)); }, [searchQuery, activeTab]); const handleTabChange = (tab: TabType) => { setActiveTab(tab); const newTabBooks = tab === 'saved' ? mockSavedBooks : mockGroupBooks; - if (searchQuery.trim() === '') { - setFilteredBooks(newTabBooks); - } else { - const filtered = newTabBooks.filter( - book => - book.title.toLowerCase().includes(searchQuery.toLowerCase()) || - book.author.toLowerCase().includes(searchQuery.toLowerCase()), - ); - setFilteredBooks(filtered); - } + setFilteredBooks(filterBooks(newTabBooks, searchQuery)); };
🧹 Nitpick comments (6)
src/components/createpost/PhotoSection.styled.ts (1)
2-2: 색상 사용의 일관성을 개선해주세요.
colors와semanticColors두 가지를 모두 import하여 혼용하고 있습니다. 일관성을 위해 가능한 한semanticColors를 우선적으로 사용하는 것을 권장합니다.예시 수정:
- border: 1px solid ${colors.grey[300]}; + border: 1px solid ${semanticColors.text.ghost}; - &:disabled { - background-color: ${colors.darkgrey.dark}; - border: 1px solid ${colors.darkgrey.main}; + &:disabled { + background-color: ${semanticColors.background.cardDark}; + border: 1px solid ${semanticColors.background.card};src/components/createpost/PostContentSection.tsx (1)
10-10: 선택적 개선: maxLength를 props로 받도록 고려현재 maxLength가 하드코딩되어 있어도 문제없지만, 컴포넌트의 재사용성을 높이려면 props로 받는 것을 고려해볼 수 있습니다.
interface PostContentSectionProps { content: string; onContentChange: (value: string) => void; + maxLength?: number; } -const PostContentSection = ({ content, onContentChange }: PostContentSectionProps) => { - const maxLength = 2000; +const PostContentSection = ({ content, onContentChange, maxLength = 2000 }: PostContentSectionProps) => {src/pages/post/CreatePost.tsx (1)
63-69: 사진 관리 로직이 적절합니다.File 객체를 직접 상태로 관리하는 현재 방식은 최대 3개 제한 하에서는 메모리 사용량 측면에서 큰 문제가 없습니다. 더 큰 규모의 업로드가 필요하다면 File의 메타데이터만 저장하고 실제 업로드 시에만 File 객체를 사용하는 방식을 고려할 수 있습니다.
src/components/createpost/PhotoSection.tsx (1)
52-52: 스타일 일관성을 위해 스타일드 컴포넌트 사용을 권장합니다.인라인 스타일 대신 스타일드 컴포넌트를 사용하여 일관성을 유지하세요.
PhotoSection.styled.ts에 추가:
export const PhotoItem = styled.div` position: relative; width: 80px; height: 80px; `;그리고 컴포넌트에서:
+import { ..., PhotoItem } from './PhotoSection.styled'; -<div key={index} style={{ position: 'relative', width: '80px', height: '80px' }}> +<PhotoItem key={index}> <PhotoImage src={createImageUrl(photo)} alt={`선택된 사진 ${index + 1}`} /> <RemoveButton onClick={() => onPhotoRemove(index)}> <img src={closeIcon} alt="삭제" /> </RemoveButton> -</div> +</PhotoItem>src/components/createpost/TagSelectionSection.tsx (2)
40-51: 태그 선택 로직을 단순화할 수 있습니다.
handleTagToggle함수에서 조건문을 중첩하여 복잡도가 높아졌습니다. 로직을 단순화하면 가독성과 유지보수성이 향상됩니다.const handleTagToggle = (tag: string) => { - // 이미 선택된 태그면 해제 - if (selectedTags.includes(tag)) { - onTagToggle(tag); - return; - } - - // 5개 미만이면 추가 가능 - if (selectedTags.length < 5) { - onTagToggle(tag); - } + // 이미 선택된 태그이거나 5개 미만일 때만 토글 허용 + if (selectedTags.includes(tag) || selectedTags.length < 5) { + onTagToggle(tag); + } };
101-101: 접근성을 개선하기 위해 적절한 alt 텍스트를 제공하세요.현재 alt 텍스트가 너무 일반적입니다. 더 구체적인 설명을 제공하면 스크린 리더 사용자의 경험이 향상됩니다.
-<img src={closeIcon} alt="삭제" /> +<img src={closeIcon} alt={`${tag} 태그 삭제`} />
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (3)
src/assets/post/close.svgis excluded by!**/*.svgsrc/assets/post/plus-disabled.svgis excluded by!**/*.svgsrc/assets/post/plus.svgis excluded by!**/*.svg
📒 Files selected for processing (12)
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx(4 hunks)src/components/createpost/PhotoSection.styled.ts(1 hunks)src/components/createpost/PhotoSection.tsx(1 hunks)src/components/createpost/PostContentSection.styled.ts(1 hunks)src/components/createpost/PostContentSection.tsx(1 hunks)src/components/createpost/PrivacyToggleSection.styled.ts(1 hunks)src/components/createpost/PrivacyToggleSection.tsx(1 hunks)src/components/createpost/TagSelectionSection.styled.ts(1 hunks)src/components/createpost/TagSelectionSection.tsx(1 hunks)src/pages/index.tsx(2 hunks)src/pages/post/CreatePost.styled.ts(1 hunks)src/pages/post/CreatePost.tsx(1 hunks)
🧬 Code Graph Analysis (11)
src/components/createpost/PrivacyToggleSection.tsx (2)
src/pages/group/CommonSection.styled.ts (1)
SectionTitle(13-18)src/components/createpost/PrivacyToggleSection.styled.ts (2)
ToggleContainer(4-8)ToggleLabel(10-14)
src/components/createpost/PrivacyToggleSection.styled.ts (1)
src/styles/global/global.ts (2)
semanticColors(80-153)typography(56-77)
src/pages/post/CreatePost.tsx (1)
src/pages/post/CreatePost.styled.ts (1)
Container(4-14)
src/pages/post/CreatePost.styled.ts (1)
src/styles/global/global.ts (1)
semanticColors(80-153)
src/components/createpost/PostContentSection.styled.ts (1)
src/styles/global/global.ts (2)
semanticColors(80-153)typography(56-77)
src/components/createpost/PhotoSection.styled.ts (1)
src/styles/global/global.ts (3)
colors(4-53)semanticColors(80-153)typography(56-77)
src/components/createpost/PostContentSection.tsx (2)
src/pages/group/CommonSection.styled.ts (1)
SectionTitle(13-18)src/components/createpost/PostContentSection.styled.ts (3)
TextAreaBox(4-8)TextArea(10-25)CharacterCount(27-33)
src/components/createpost/TagSelectionSection.styled.ts (1)
src/styles/global/global.ts (3)
semanticColors(80-153)colors(4-53)typography(56-77)
src/components/createpost/TagSelectionSection.tsx (2)
src/pages/group/CommonSection.styled.ts (1)
SectionTitle(13-18)src/components/createpost/TagSelectionSection.styled.ts (10)
TagContainer(4-8)GenreButtonGroup(10-14)GenreButton(16-32)SubTagGrid(34-38)SubTagButton(40-60)TagCount(62-67)SelectedTagsSection(69-73)SelectedTagsTitle(75-79)SelectedTagList(81-85)SelectedTagItem(87-109)
src/components/createpost/PhotoSection.tsx (2)
src/pages/group/CommonSection.styled.ts (1)
SectionTitle(13-18)src/components/createpost/PhotoSection.styled.ts (6)
PhotoContainer(4-8)PhotoGrid(10-14)AddPhotoButton(16-41)PhotoImage(49-54)RemoveButton(56-78)PhotoCount(80-85)
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx (1)
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts (2)
TabContainer(104-109)Tab(111-134)
🪛 ESLint
src/pages/post/CreatePost.tsx
[error] 16-16: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 59-59: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🧰 Additional context used
🧬 Code Graph Analysis (11)
src/components/createpost/PrivacyToggleSection.tsx (2)
src/pages/group/CommonSection.styled.ts (1)
SectionTitle(13-18)src/components/createpost/PrivacyToggleSection.styled.ts (2)
ToggleContainer(4-8)ToggleLabel(10-14)
src/components/createpost/PrivacyToggleSection.styled.ts (1)
src/styles/global/global.ts (2)
semanticColors(80-153)typography(56-77)
src/pages/post/CreatePost.tsx (1)
src/pages/post/CreatePost.styled.ts (1)
Container(4-14)
src/pages/post/CreatePost.styled.ts (1)
src/styles/global/global.ts (1)
semanticColors(80-153)
src/components/createpost/PostContentSection.styled.ts (1)
src/styles/global/global.ts (2)
semanticColors(80-153)typography(56-77)
src/components/createpost/PhotoSection.styled.ts (1)
src/styles/global/global.ts (3)
colors(4-53)semanticColors(80-153)typography(56-77)
src/components/createpost/PostContentSection.tsx (2)
src/pages/group/CommonSection.styled.ts (1)
SectionTitle(13-18)src/components/createpost/PostContentSection.styled.ts (3)
TextAreaBox(4-8)TextArea(10-25)CharacterCount(27-33)
src/components/createpost/TagSelectionSection.styled.ts (1)
src/styles/global/global.ts (3)
semanticColors(80-153)colors(4-53)typography(56-77)
src/components/createpost/TagSelectionSection.tsx (2)
src/pages/group/CommonSection.styled.ts (1)
SectionTitle(13-18)src/components/createpost/TagSelectionSection.styled.ts (10)
TagContainer(4-8)GenreButtonGroup(10-14)GenreButton(16-32)SubTagGrid(34-38)SubTagButton(40-60)TagCount(62-67)SelectedTagsSection(69-73)SelectedTagsTitle(75-79)SelectedTagList(81-85)SelectedTagItem(87-109)
src/components/createpost/PhotoSection.tsx (2)
src/pages/group/CommonSection.styled.ts (1)
SectionTitle(13-18)src/components/createpost/PhotoSection.styled.ts (6)
PhotoContainer(4-8)PhotoGrid(10-14)AddPhotoButton(16-41)PhotoImage(49-54)RemoveButton(56-78)PhotoCount(80-85)
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx (1)
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts (2)
TabContainer(104-109)Tab(111-134)
🪛 ESLint
src/pages/post/CreatePost.tsx
[error] 16-16: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 59-59: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🔇 Additional comments (15)
src/pages/post/CreatePost.styled.ts (1)
1-14: 코드 구조와 스타일링이 잘 구성되었습니다.반응형 디자인을 고려한 컨테이너 스타일링이 적절하며, 전역 스타일 시스템을 올바르게 활용하고 있습니다. box-sizing과 flexbox 레이아웃 설정도 적절합니다.
src/components/createpost/PostContentSection.styled.ts (1)
1-33: 텍스트 입력 영역 스타일링이 일관성 있게 구현되었습니다.전역 테마 시스템을 올바르게 활용하고, textarea의 기본 스타일을 적절히 제거했습니다. 문자 수 카운터의 위치와 스타일링도 적절합니다.
src/components/createpost/PrivacyToggleSection.styled.ts (1)
1-36: 토글 스위치 컴포넌트가 잘 구현되었습니다.상태 기반 스타일링과 부드러운 전환 애니메이션이 적절히 적용되었으며, 타입스크립트 props를 올바르게 활용하고 있습니다. 테마 시스템 사용도 일관성이 있습니다.
src/components/createpost/PhotoSection.styled.ts (2)
16-41: 사진 추가 버튼의 상호작용 스타일링이 잘 구현되었습니다.hover 및 disabled 상태에 대한 적절한 스타일링이 적용되어 사용자 경험이 향상될 것입니다.
56-78: 제거 버튼의 위치와 스타일링이 적절합니다.절대 위치 지정과 반투명 배경을 통해 사용자가 쉽게 인식할 수 있도록 구현되었습니다.
src/pages/index.tsx (2)
13-13: 새로운 CreatePost 컴포넌트 import가 올바르게 추가되었습니다.
39-39: post/create 라우트가 적절히 추가되었습니다.새로운 게시글 생성 기능을 위한 라우트가 기존 구조와 일관성 있게 추가되었습니다.
src/components/createpost/PrivacyToggleSection.tsx (1)
1-28: 컴포넌트 구현이 깔끔합니다.단일 책임 원칙을 잘 따르고 있으며, 타입 안전성과 재사용성을 모두 확보한 좋은 구현입니다. 기존 공통 컴포넌트를 재사용하여 코드 중복을 방지한 점도 좋습니다.
src/components/createpost/PostContentSection.tsx (1)
1-31: 구현이 잘 되어 있습니다.텍스트 입력과 실시간 글자 수 표시 기능이 잘 구현되어 있습니다. 사용자 경험을 고려한 placeholder와 적절한 rows 설정도 좋습니다.
src/pages/post/CreatePost.tsx (1)
82-127: 전체 구조가 잘 설계되었습니다.각 섹션이 독립적인 컴포넌트로 분리되어 있어 유지보수성이 좋고, 공통 컴포넌트의 재사용으로 코드 중복을 방지했습니다. 폼 검증 로직과 완료 버튼 활성화 조건도 적절합니다.
src/components/createpost/PhotoSection.tsx (1)
21-74: 사진 업로드 기능이 잘 구현되었습니다.파일 업로드 UX가 좋고, 최대 3개 제한과 미리보기 기능이 적절히 구현되어 있습니다. 접근성과 사용성도 잘 고려되었습니다.
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx (2)
40-80: 탭 기능을 위한 데이터 분리가 적절합니다.
mockSavedBooks와mockGroupBooks로 분리하여 각 탭별로 다른 책 목록을 제공할 수 있도록 구조를 개선했습니다.
194-204: 조건부 탭 렌더링이 좋은 UX 개선입니다.검색어가 있을 때 탭을 숨기는 것은 사용자가 현재 상태를 명확히 이해할 수 있도록 돕는 좋은 결정입니다.
src/components/createpost/TagSelectionSection.styled.ts (1)
1-109: 잘 구조화된 스타일 컴포넌트 파일입니다.전역 디자인 시스템을 일관성 있게 활용하고, 각 컴포넌트의 역할이 명확하게 분리되어 있습니다. 상태별 스타일링(active, disabled, hover)도 적절히 구현되었습니다.
src/components/createpost/TagSelectionSection.tsx (1)
82-82: 태그 비활성화 로직이 직관적입니다.선택되지 않은 태그가 5개 제한에 도달했을 때만 비활성화하는 로직이 매우 적절합니다. 이미 선택된 태그는 항상 해제 가능하도록 구현된 점이 좋습니다.
| const createImageUrl = (file: File) => { | ||
| return URL.createObjectURL(file); | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
메모리 누수 방지를 위해 URL 해제가 필요합니다.
URL.createObjectURL로 생성된 blob URL은 수동으로 해제해야 메모리 누수를 방지할 수 있습니다.
+import { useRef, useEffect } from 'react';
const PhotoSection = ({ photos, onPhotoAdd, onPhotoRemove }: PhotoSectionProps) => {
const fileInputRef = useRef<HTMLInputElement>(null);
+ const urlsRef = useRef<string[]>([]);
+ useEffect(() => {
+ // 컴포넌트 언마운트 시 모든 URL 해제
+ return () => {
+ urlsRef.current.forEach(url => URL.revokeObjectURL(url));
+ };
+ }, []);
+ useEffect(() => {
+ // 사진 배열이 변경될 때마다 URL 업데이트
+ urlsRef.current.forEach(url => URL.revokeObjectURL(url));
+ urlsRef.current = photos.map(photo => URL.createObjectURL(photo));
+ }, [photos]);
const createImageUrl = (file: File) => {
- return URL.createObjectURL(file);
+ const index = photos.indexOf(file);
+ return urlsRef.current[index] || URL.createObjectURL(file);
};Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/components/createpost/PhotoSection.tsx around lines 37 to 39, the
function createImageUrl uses URL.createObjectURL to generate a blob URL but does
not release it, which can cause memory leaks. Modify the code to call
URL.revokeObjectURL on the created URL when it is no longer needed, such as
during component cleanup or when the image is removed, to properly release the
allocated memory.
| const genreTagsMap: Record<string, string[]> = { | ||
| 문학: ['소설', '시', '에세이', '인문학', '철학'], | ||
| '과학·IT': ['기술', '과학', 'AI', '데이터'], | ||
| 사회과학: ['정치', '경제', '사회학', '심리학', '역사'], | ||
| 인문학: ['철학', '역사', '문화', '언어학', '종교'], | ||
| 예술: ['미술', '음악', '영화', '디자인', '사진'], | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
장르별 태그 매핑에 중복된 태그가 있습니다.
genreTagsMap에서 '철학'과 '역사' 태그가 여러 장르에 중복되어 있습니다:
- '철학': 문학, 인문학
- '역사': 사회과학, 인문학
이는 사용자 혼란을 야기하고 태그 선택 시 예상치 못한 동작을 일으킬 수 있습니다.
다음 중 하나의 방식으로 개선하는 것을 권장합니다:
const genreTagsMap: Record<string, string[]> = {
- 문학: ['소설', '시', '에세이', '인문학', '철학'],
+ 문학: ['소설', '시', '에세이', '문학이론'],
'과학·IT': ['기술', '과학', 'AI', '데이터'],
- 사회과학: ['정치', '경제', '사회학', '심리학', '역사'],
+ 사회과학: ['정치', '경제', '사회학', '심리학'],
- 인문학: ['철학', '역사', '문화', '언어학', '종교'],
+ 인문학: ['철학', '역사', '문화', '언어학', '종교'],
예술: ['미술', '음악', '영화', '디자인', '사진'],
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const genreTagsMap: Record<string, string[]> = { | |
| 문학: ['소설', '시', '에세이', '인문학', '철학'], | |
| '과학·IT': ['기술', '과학', 'AI', '데이터'], | |
| 사회과학: ['정치', '경제', '사회학', '심리학', '역사'], | |
| 인문학: ['철학', '역사', '문화', '언어학', '종교'], | |
| 예술: ['미술', '음악', '영화', '디자인', '사진'], | |
| }; | |
| const genreTagsMap: Record<string, string[]> = { | |
| 문학: ['소설', '시', '에세이', '문학이론'], | |
| '과학·IT': ['기술', '과학', 'AI', '데이터'], | |
| 사회과학: ['정치', '경제', '사회학', '심리학'], | |
| 인문학: ['철학', '역사', '문화', '언어학', '종교'], | |
| 예술: ['미술', '음악', '영화', '디자인', '사진'], | |
| }; |
🤖 Prompt for AI Agents
In src/components/createpost/TagSelectionSection.tsx around lines 23 to 29, the
genreTagsMap contains duplicate tags like '철학' and '역사' appearing in multiple
genres, which can confuse users and cause unexpected behavior. To fix this,
review the genre categories and assign each tag to only one appropriate genre,
removing duplicates to ensure tags are unique across genres. Alternatively,
consider restructuring the data model to allow tags to belong to multiple genres
explicitly if that fits the use case better.
| const handleSelectedTagRemove = (tag: string) => { | ||
| onTagToggle(tag); | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
중복된 함수를 제거하세요.
handleSelectedTagRemove 함수는 handleTagToggle과 동일한 작업을 수행합니다. 불필요한 중복 함수입니다.
-const handleSelectedTagRemove = (tag: string) => {
- onTagToggle(tag);
-};
// 그리고 JSX에서 직접 호출
-<SelectedTagItem key={tag} onClick={() => handleSelectedTagRemove(tag)}>
+<SelectedTagItem key={tag} onClick={() => onTagToggle(tag)}>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleSelectedTagRemove = (tag: string) => { | |
| onTagToggle(tag); | |
| }; | |
| // --- remove these lines entirely (they are no longer needed) --- | |
| // const handleSelectedTagRemove = (tag: string) => { | |
| // onTagToggle(tag); | |
| // }; | |
| // ------------------------------------------------------------- | |
| // …later, in your JSX mapping selectedTags… | |
| {selectedTags.map(tag => ( | |
| <SelectedTagItem | |
| key={tag} | |
| onClick={() => onTagToggle(tag)} | |
| > | |
| {tag} | |
| </SelectedTagItem> | |
| ))} |
🤖 Prompt for AI Agents
In src/components/createpost/TagSelectionSection.tsx around lines 53 to 55, the
function handleSelectedTagRemove duplicates the functionality of
handleTagToggle. Remove the handleSelectedTagRemove function entirely and
replace its usage with handleTagToggle to eliminate redundancy.
#️⃣연관된 이슈
📝작업 내용
주요 구현 기능
1️⃣ 책 선택 기능
기존 BookSelectionSection 컴포넌트를 재사용하여 BookSearchBottomSheet를 통한 책 검색 및 선택 기능을 구현했습니다. 사용자는 "저장한 책"과 "모임 책" 필터를 통해 원하는 책을 쉽게 찾을 수 있으며, 검색어 입력 시에는 선택된 필터 내에서만 검색이 이루어지도록 리팩토링 했습니다.
2️⃣ 글 작성 영역
PostContentSection 컴포넌트를 새로 개발하여 2000자 제한의 텍스트 입력이 가능합니다. 실시간 글자 수 카운터를 포함하고 있습니다.
3️⃣ 사진 첨부 기능
PhotoSection 컴포넌트를 통해 최대 3개까지 사진을 업로드할 수 있는 기능을 구현했습니다. 업로드된 사진은 미리보기와 함께 표시되며, 각 사진 우상단에 위치한 close.svg 아이콘을 통해 개별 삭제가 가능합니다.
4️⃣ 공개 설정
PrivacyToggleSection 컴포넌트를 새로 개발하여 글의 공개/비공개 설정을 간단한 토글 스위치로 제어할 수 있도록 했습니다. 모임 생성과 달리 비밀번호 없이 단순히 전체 피드 노출 여부만 설정할 수 있어 사용자 경험을 단순화했습니다.
5️⃣ 2단계 태그 시스템
TagSelectionSection 컴포넌트에서 가장 복잡한 기능을 구현했습니다. 상위 장르(문학, 과학·IT, 사회과학, 인문학, 예술) 선택 시 해당하는 하위 태그들이 표시되며, 사용자는 장르 상관없이 최대 5개까지 태그를 선택할 수 있습니다. 5개 선택 시 나머지 태그들은 자동으로 비활성화되어 6개 이상 선택을 방지합니다.
기술적 구현 사항
컴포넌트 재사용성
기존 프로젝트의 BookSelectionSection, Section, TitleHeader 등의 컴포넌트를 최대한 재사용하여 코드 중복을 줄이고 일관성 있는 UI를 구현했습니다. 특히 모임 생성과 글 작성의 차이점을 고려하여 필요한 부분만 새로운 컴포넌트로 분리했습니다.
상태 관리
React useState를 활용하여 각 섹션별 상태를 효율적으로 관리했습니다. 책 선택, 글 내용, 사진 파일 배열, 공개 설정, 선택된 태그 배열 등의 상태를 독립적으로 관리하면서도 전체적인 폼 유효성 검증을 구현했습니다.
사용자 경험 최적화
필수 항목(책 선택, 글 내용)이 모두 입력된 경우에만 완료 버튼이 활성화되도록 하여 사용자가 명확하게 작성 진행 상황을 파악할 수 있도록 했습니다. 각 섹션별로 적절한 안내 텍스트와 카운터를 제공하여 사용성을 높였습니다.
스크린샷 (선택)
비디오 올리려고 했는데 용량 이슈..
💬리뷰 요구사항(선택)
사진 업로드 최적화 - 현재 File 객체를 그대로 상태로 관리하고 있는데, 메모리 사용량이나 성능 측면에서 개선할 부분이 있는지 확인해주시면 좋겠습니다.
Summary by CodeRabbit
신규 기능
스타일
버그 수정
라우팅