diff --git a/src/assets/post/close.svg b/src/assets/post/close.svg new file mode 100644 index 00000000..517027ec --- /dev/null +++ b/src/assets/post/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/post/plus-disabled.svg b/src/assets/post/plus-disabled.svg new file mode 100644 index 00000000..9ab32165 --- /dev/null +++ b/src/assets/post/plus-disabled.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/post/plus.svg b/src/assets/post/plus.svg new file mode 100644 index 00000000..3910e81c --- /dev/null +++ b/src/assets/post/plus.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx b/src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx index bc3a3c19..b8aa5dff 100644 --- a/src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx +++ b/src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx @@ -37,7 +37,7 @@ interface BookSearchBottomSheetProps { type TabType = 'saved' | 'group'; // Mock Data -const mockBooks: Book[] = [ +const mockSavedBooks: Book[] = [ { id: 1, title: '토마토 컵라면', @@ -56,51 +56,53 @@ const mockBooks: Book[] = [ author: '작가명', cover: '/src/assets/books/hormone.svg', }, +]; + +const mockGroupBooks: Book[] = [ { id: 4, - title: '토마토 컵라면', + title: '단 한번의 삶', author: '작가명', - cover: '/src/assets/books/tomato.svg', + cover: '/src/assets/books/life.svg', }, { id: 5, - title: '사슴', - author: '작가명', - cover: '/src/assets/books/deer.svg', - }, - { - id: 6, title: '호르몬 체인지', author: '작가명', cover: '/src/assets/books/hormone.svg', }, { - id: 7, - title: '단 한번의 삶', + id: 6, + title: '토마토 컵라면', author: '작가명', - cover: '/src/assets/books/life.svg', + cover: '/src/assets/books/tomato.svg', }, ]; const BookSearchBottomSheet = ({ isOpen, onClose, onSelectBook }: BookSearchBottomSheetProps) => { // State const [searchQuery, setSearchQuery] = useState(''); - const [filteredBooks, setFilteredBooks] = useState(mockBooks); + const [filteredBooks, setFilteredBooks] = useState(mockSavedBooks); const [activeTab, setActiveTab] = useState('saved'); // Effects useEffect(() => { + // 현재 활성화된 탭의 책 목록 가져오기 + const currentTabBooks = activeTab === 'saved' ? mockSavedBooks : mockGroupBooks; + if (searchQuery.trim() === '') { - setFilteredBooks(mockBooks); + // 검색어가 없을 때는 선택된 탭의 전체 목록 표시 + setFilteredBooks(currentTabBooks); } else { - const filtered = mockBooks.filter( + // 검색어가 있을 때는 선택된 탭 내에서만 검색 + const filtered = currentTabBooks.filter( book => book.title.toLowerCase().includes(searchQuery.toLowerCase()) || book.author.toLowerCase().includes(searchQuery.toLowerCase()), ); setFilteredBooks(filtered); } - }, [searchQuery]); + }, [searchQuery, activeTab]); useEffect(() => { if (isOpen) { @@ -145,6 +147,26 @@ const BookSearchBottomSheet = ({ isOpen, onClose, onSelectBook }: BookSearchBott setSearchQuery(''); }; + 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); + } + }; + + // 검색어가 없을 때만 탭 표시 + const showTabs = searchQuery.trim() === ''; + return ( @@ -169,15 +191,17 @@ const BookSearchBottomSheet = ({ isOpen, onClose, onSelectBook }: BookSearchBott - {/* 탭 영역 */} - - setActiveTab('saved')}> - 저장한 책 - - setActiveTab('group')}> - 모임 책 - - + {/* 탭 영역 - 검색어가 없을 때만 표시 */} + {showTabs && ( + + handleTabChange('saved')}> + 저장한 책 + + handleTabChange('group')}> + 모임 책 + + + )} {/* 책 목록 영역 */} diff --git a/src/components/createpost/PhotoSection.styled.ts b/src/components/createpost/PhotoSection.styled.ts new file mode 100644 index 00000000..95164f97 --- /dev/null +++ b/src/components/createpost/PhotoSection.styled.ts @@ -0,0 +1,85 @@ +import styled from '@emotion/styled'; +import { typography, semanticColors, colors } from '../../styles/global/global'; + +export const PhotoContainer = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + +export const PhotoGrid = styled.div` + display: flex; + gap: 12px; + align-items: center; +`; + +export const AddPhotoButton = styled.button` + width: 80px; + height: 80px; + border: 1px solid ${colors.grey[300]}; + background-color: ${semanticColors.background.cardDark}; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + + img { + width: 24px; + height: 24px; + } + + &:hover:not(:disabled) { + border-color: ${semanticColors.text.primary}; + } + + &:disabled { + background-color: ${colors.darkgrey.dark}; + border: 1px solid ${colors.darkgrey.main}; + cursor: not-allowed; + } +`; + +export const PhotoItem = styled.div` + position: relative; + width: 80px; + height: 80px; +`; + +export const PhotoImage = styled.img` + width: 100%; + height: 100%; + object-fit: cover; + border: 1px solid ${colors.grey[300]}; +`; + +export const RemoveButton = styled.button` + position: absolute; + top: 0px; + right: 0px; + width: 24px; + height: 24px; + background-color: rgba(0, 0, 0, 0.6); + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid ${colors.grey[300]}; + + img { + width: 12px; + height: 12px; + } + + &:hover { + background-color: rgba(0, 0, 0, 0.8); + } +`; + +export const PhotoCount = styled.div` + align-self: flex-end; + color: ${semanticColors.text.point.green}; + font-size: ${typography.fontSize.xs}; + font-weight: ${typography.fontWeight.regular}; +`; diff --git a/src/components/createpost/PhotoSection.tsx b/src/components/createpost/PhotoSection.tsx new file mode 100644 index 00000000..5a3cd9dc --- /dev/null +++ b/src/components/createpost/PhotoSection.tsx @@ -0,0 +1,74 @@ +import { useRef } from 'react'; +import { Section, SectionTitle } from '../../pages/group/CommonSection.styled'; +import { + PhotoContainer, + PhotoGrid, + AddPhotoButton, + PhotoImage, + RemoveButton, + PhotoCount, +} from './PhotoSection.styled'; +import plusIcon from '../../assets/post/plus.svg'; +import plusDisabledIcon from '../../assets/post/plus-disabled.svg'; +import closeIcon from '../../assets/post/close.svg'; + +interface PhotoSectionProps { + photos: File[]; + onPhotoAdd: (files: File[]) => void; + onPhotoRemove: (index: number) => void; +} + +const PhotoSection = ({ photos, onPhotoAdd, onPhotoRemove }: PhotoSectionProps) => { + const fileInputRef = useRef(null); + + const handleFileInputClick = () => { + fileInputRef.current?.click(); + }; + + const handleFileChange = (e: React.ChangeEvent) => { + const files = Array.from(e.target.files || []); + if (files.length > 0) { + onPhotoAdd(files); + } + // input 값 초기화 (같은 파일을 다시 선택할 수 있도록) + e.target.value = ''; + }; + + const createImageUrl = (file: File) => { + return URL.createObjectURL(file); + }; + + const isDisabled = photos.length >= 3; + + return ( +
+ 사진 추가 + + + + 사진 추가 + + {photos.map((photo, index) => ( +
+ + onPhotoRemove(index)}> + 삭제 + +
+ ))} +
+ {photos.length}/3개 + +
+
+ ); +}; + +export default PhotoSection; diff --git a/src/components/createpost/PostContentSection.styled.ts b/src/components/createpost/PostContentSection.styled.ts new file mode 100644 index 00000000..15b52217 --- /dev/null +++ b/src/components/createpost/PostContentSection.styled.ts @@ -0,0 +1,33 @@ +import styled from '@emotion/styled'; +import { typography, semanticColors } from '../../styles/global/global'; + +export const TextAreaBox = styled.div` + position: relative; + display: flex; + flex-direction: column; +`; + +export const TextArea = styled.textarea` + width: 100%; + min-height: 100px; + background-color: ${semanticColors.background.primary}; + color: ${semanticColors.text.secondary}; + font-size: ${typography.fontSize.sm}; + font-weight: ${typography.fontWeight.regular}; + font-family: ${typography.fontFamily.primary}; + resize: none; + outline: none; + border: none; + + &::placeholder { + color: ${semanticColors.text.ghost}; + } +`; + +export const CharacterCount = styled.div` + align-self: flex-end; + margin-top: 12px; + color: ${semanticColors.text.point.green}; + font-size: ${typography.fontSize.xs}; + font-weight: ${typography.fontWeight.regular}; +`; diff --git a/src/components/createpost/PostContentSection.tsx b/src/components/createpost/PostContentSection.tsx new file mode 100644 index 00000000..4579e034 --- /dev/null +++ b/src/components/createpost/PostContentSection.tsx @@ -0,0 +1,31 @@ +import { Section, SectionTitle } from '../../pages/group/CommonSection.styled'; +import { TextAreaBox, TextArea, CharacterCount } from './PostContentSection.styled'; + +interface PostContentSectionProps { + content: string; + onContentChange: (value: string) => void; +} + +const PostContentSection = ({ content, onContentChange }: PostContentSectionProps) => { + const maxLength = 2000; + + return ( +
+ 글 작성 + +