diff --git a/src/components/common/BookSearchBottomSheet/BookSearchStates.tsx b/src/components/common/BookSearchBottomSheet/BookSearchStates.tsx
index f67b2729..20c7c68a 100644
--- a/src/components/common/BookSearchBottomSheet/BookSearchStates.tsx
+++ b/src/components/common/BookSearchBottomSheet/BookSearchStates.tsx
@@ -44,7 +44,7 @@ const BookSearchStates = ({ isLoading, error, isEmpty, onClose }: BookSearchStat
if (isEmpty) {
return (
- 현재 등록된 책이 아닙니다.
+ 현재 등록된 책이 없습니다.
원하시는 책을 신청해주세요.
책 신청하기
diff --git a/src/components/creategroup/ActivityPeriodSection/ActivityPeriodSection.tsx b/src/components/creategroup/ActivityPeriodSection/ActivityPeriodSection.tsx
index 8d1d3895..9b24e91d 100644
--- a/src/components/creategroup/ActivityPeriodSection/ActivityPeriodSection.tsx
+++ b/src/components/creategroup/ActivityPeriodSection/ActivityPeriodSection.tsx
@@ -16,6 +16,7 @@ interface ActivityPeriodSectionProps {
endDate: { year: number; month: number; day: number };
onStartDateChange: (date: { year: number; month: number; day: number }) => void;
onEndDateChange: (date: { year: number; month: number; day: number }) => void;
+ onValidationChange?: (isValid: boolean) => void;
}
const ActivityPeriodSection = ({
@@ -23,6 +24,7 @@ const ActivityPeriodSection = ({
endDate,
onStartDateChange,
onEndDateChange,
+ onValidationChange,
}: ActivityPeriodSectionProps) => {
// 현재 년도와 다음 년도
const currentYear = new Date().getFullYear();
@@ -63,6 +65,16 @@ const ActivityPeriodSection = ({
return endDateObj < startDateObj;
}, [startDate, endDate]);
+ // 날짜 유효성 검사 결과
+ const isDateValid = !isOverMaxDays && !isEndDateBeforeStart;
+
+ // 유효성 변경 시 부모에게 알림
+ useEffect(() => {
+ if (onValidationChange) {
+ onValidationChange(isDateValid);
+ }
+ }, [isDateValid, onValidationChange]);
+
// 오늘 날짜로 초기값 설정
const getInitialDate = () => {
const today = new Date();
diff --git a/src/components/createpost/PostContentSection.styled.ts b/src/components/createpost/PostContentSection.styled.ts
index 4522c73f..a1ac59cf 100644
--- a/src/components/createpost/PostContentSection.styled.ts
+++ b/src/components/createpost/PostContentSection.styled.ts
@@ -10,7 +10,7 @@ export const TextAreaBox = styled.div`
export const TextArea = styled.textarea<{ readOnly?: boolean }>`
width: 100%;
min-height: 100px;
- background-color: ${props => props.readOnly ? '#f5f5f5' : semanticColors.background.primary};
+ background-color: ${props => (props.readOnly ? '#f5f5f5' : semanticColors.background.primary)};
color: ${semanticColors.text.secondary};
font-size: ${typography.fontSize.sm};
font-weight: ${typography.fontWeight.regular};
@@ -19,7 +19,8 @@ export const TextArea = styled.textarea<{ readOnly?: boolean }>`
outline: none;
border: none;
overflow: hidden;
- cursor: ${props => props.readOnly ? 'not-allowed' : 'text'};
+ cursor: ${props => (props.readOnly ? 'not-allowed' : 'text')};
+ padding: 0;
&::placeholder {
color: ${semanticColors.text.ghost};
diff --git a/src/components/group/GroupBookSection.styled.ts b/src/components/group/GroupBookSection.styled.ts
index 0012b6e5..09cd9b32 100644
--- a/src/components/group/GroupBookSection.styled.ts
+++ b/src/components/group/GroupBookSection.styled.ts
@@ -19,12 +19,20 @@ export const BookTitle = styled.h3`
font-weight: ${typography.fontWeight.semibold};
margin: 0;
margin-right: 12px;
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
`;
export const BookAuthor = styled.span`
color: ${semanticColors.text.secondary};
font-size: ${typography.fontSize.xs};
font-weight: ${typography.fontWeight.regular};
+ max-width: 120px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
`;
export const ChevronIcon = styled.img`
@@ -37,4 +45,6 @@ export const RightSection = styled.div`
display: flex;
align-items: center;
margin-right: -8px;
+ flex-shrink: 0;
+ gap: 8px;
`;
diff --git a/src/components/group/GroupCard.tsx b/src/components/group/GroupCard.tsx
index c77af0cc..4490ae1d 100644
--- a/src/components/group/GroupCard.tsx
+++ b/src/components/group/GroupCard.tsx
@@ -77,6 +77,7 @@ const CoverWrapper = styled.div`
const Cover = styled.img<{ cardType: 'main' | 'search' | 'modal'; isRecommend?: boolean }>`
object-fit: cover;
+ object-position: top;
flex-shrink: 0;
width: ${({ cardType, isRecommend }) => (cardType === 'search' || isRecommend ? '60px' : '80px')};
height: ${({ cardType, isRecommend }) =>
diff --git a/src/components/memory/RecordItem/PollRecord.tsx b/src/components/memory/RecordItem/PollRecord.tsx
index a21bc333..d714ce09 100644
--- a/src/components/memory/RecordItem/PollRecord.tsx
+++ b/src/components/memory/RecordItem/PollRecord.tsx
@@ -141,6 +141,9 @@ const PollRecord = ({ content, pollOptions, postId, shouldBlur = false, onVoteUp
}
};
+ // 아무도 투표하지 않았는지 확인 (모든 옵션이 0%인지 확인)
+ const hasVotes = currentOptions.some(option => option.percentage > 0);
+
return (
{content}
@@ -148,7 +151,7 @@ const PollRecord = ({ content, pollOptions, postId, shouldBlur = false, onVoteUp
{currentOptions.map((option, index) => (
handleOptionClick(option)}
style={{
cursor: shouldBlur ? 'default' : (isVoting ? 'not-allowed' : 'pointer'),
@@ -158,22 +161,24 @@ const PollRecord = ({ content, pollOptions, postId, shouldBlur = false, onVoteUp
>
-
- {option.id}
+
+ {index + 1}
-
+
{option.text}
-
- {option.percentage}%
-
+ {hasVotes && (
+
+ {option.percentage}%
+
+ )}
))}
diff --git a/src/components/memory/RecordItem/RecordItem.styled.ts b/src/components/memory/RecordItem/RecordItem.styled.ts
index ac8de1da..dd293a2e 100644
--- a/src/components/memory/RecordItem/RecordItem.styled.ts
+++ b/src/components/memory/RecordItem/RecordItem.styled.ts
@@ -14,11 +14,14 @@ export const UserSection = styled.div`
margin-bottom: 8px;
`;
-export const UserAvatar = styled.div`
+export const UserAvatar = styled.div<{ src?: string }>`
width: 36px;
height: 36px;
border-radius: 50%;
background-color: ${colors.grey[400]};
+ background-image: ${({ src }) => src ? `url(${src})` : 'none'};
+ background-size: cover;
+ background-position: center;
margin-right: 8px;
`;
diff --git a/src/components/memory/RecordItem/RecordItem.tsx b/src/components/memory/RecordItem/RecordItem.tsx
index 71368449..b41ff663 100644
--- a/src/components/memory/RecordItem/RecordItem.tsx
+++ b/src/components/memory/RecordItem/RecordItem.tsx
@@ -1,4 +1,4 @@
-import { useState, useRef, useCallback } from 'react';
+import { useState, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import type { Record } from '../../../types/memory';
import TextRecord from './TextRecord';
@@ -39,6 +39,7 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => {
const {
id,
user,
+ profileImageUrl,
content,
likeCount,
commentCount,
@@ -57,11 +58,6 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => {
// 전역 댓글 바텀시트
const { openCommentBottomSheet } = useCommentBottomSheetStore();
- // 길게 누르기 상태 관리
- const [isPressed, setIsPressed] = useState(false);
- const longPressTimer = useRef(null);
- const pressStartPos = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
- const hasTriggeredLongPress = useRef(false);
// API에서 받은 isWriter 속성으로 내 기록인지 판단
const isMyRecord = isWriter ?? false;
@@ -269,94 +265,44 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => {
openCommentBottomSheet(parseInt(id), type === 'poll' ? 'VOTE' : 'RECORD');
}, [openCommentBottomSheet, id, type]);
- // 길게 누르기 이벤트 핸들러
- const handleTouchStart = useCallback(
- (e: React.TouchEvent) => {
- setIsPressed(true);
- hasTriggeredLongPress.current = false;
- pressStartPos.current = {
- x: e.touches[0].clientX,
- y: e.touches[0].clientY,
- };
-
- longPressTimer.current = setTimeout(() => {
- hasTriggeredLongPress.current = true;
- setIsPressed(false);
-
- if (isMyRecord) {
- openMoreMenu({
- onEdit: handleEdit,
- onDelete: handleDeleteConfirm,
- onPin: handlePinConfirm,
- onClose: closePopup,
- type: 'post',
- isWriter: true,
- });
- } else {
- openMoreMenu({
- onReport: handleReport,
- onClose: closePopup,
- });
- }
- }, 800);
- },
- [
- isMyRecord,
- openMoreMenu,
- handleReport,
- handleEdit,
- handleDeleteConfirm,
- handlePinConfirm,
- closePopup,
- ],
- );
-
- const handleTouchMove = useCallback((e: React.TouchEvent) => {
- if (!longPressTimer.current) return;
-
- const currentX = e.touches[0].clientX;
- const currentY = e.touches[0].clientY;
- const deltaX = Math.abs(currentX - pressStartPos.current.x);
- const deltaY = Math.abs(currentY - pressStartPos.current.y);
-
- if (deltaX > 10 || deltaY > 10) {
- clearTimeout(longPressTimer.current);
- longPressTimer.current = null;
- setIsPressed(false);
- }
- }, []);
-
- const handleTouchEnd = useCallback(() => {
- if (longPressTimer.current) {
- clearTimeout(longPressTimer.current);
- longPressTimer.current = null;
- }
- setIsPressed(false);
- }, []);
const handleClick = useCallback(() => {
- if (hasTriggeredLongPress.current) {
- hasTriggeredLongPress.current = false;
- return;
+ // 클릭으로 더보기 메뉴 표시
+ if (isMyRecord) {
+ openMoreMenu({
+ onEdit: handleEdit,
+ onDelete: handleDeleteConfirm,
+ onPin: handlePinConfirm,
+ onClose: closePopup,
+ type: 'post',
+ isWriter: true,
+ });
+ } else {
+ openMoreMenu({
+ onReport: handleReport,
+ onClose: closePopup,
+ });
}
- // 모든 기록(내 기록 포함)은 이제 길게 누르기로만 더보기 메뉴 표시
- }, []);
+ }, [
+ isMyRecord,
+ openMoreMenu,
+ handleReport,
+ handleEdit,
+ handleDeleteConfirm,
+ handlePinConfirm,
+ closePopup,
+ ]);
return (
-
+
{user}
{renderPageInfo()}
diff --git a/src/components/recordwrite/PageRangeSection.styled.ts b/src/components/recordwrite/PageRangeSection.styled.ts
index 564078e0..87d06c11 100644
--- a/src/components/recordwrite/PageRangeSection.styled.ts
+++ b/src/components/recordwrite/PageRangeSection.styled.ts
@@ -39,6 +39,7 @@ export const InputWrapper = styled.div`
font-size: ${typography.fontSize.sm};
font-weight: ${typography.fontWeight.regular};
font-family: ${typography.fontFamily.primary};
+ width: fit-content;
`;
export const PageInput = styled.input<{ inputLength?: number }>`
@@ -49,11 +50,12 @@ export const PageInput = styled.input<{ inputLength?: number }>`
font-size: ${typography.fontSize.sm};
font-weight: ${typography.fontWeight.regular};
font-family: ${typography.fontFamily.primary};
- width: ${props => (props.inputLength ? `${Math.max(30, props.inputLength * 8 + 10)}px` : '30px')};
+ width: ${props => (props.inputLength ? `${Math.max(27, props.inputLength * 8)}px` : '27px')};
padding: 0;
margin: 0;
caret-color: ${colors.white};
transition: width 0.2s ease;
+ flex-shrink: 0;
&::placeholder {
color: ${semanticColors.text.ghost};
@@ -77,6 +79,10 @@ export const PageSuffix = styled.span`
font-family: ${typography.fontFamily.primary};
margin: 0;
padding: 0;
+ white-space: nowrap;
+ display: inline-block;
+ flex-shrink: 0;
+ margin-left: 0px;
`;
export const OverallRangeText = styled.div`
diff --git a/src/components/today-words/MessageList/MessageList.tsx b/src/components/today-words/MessageList/MessageList.tsx
index 10a8b703..3f505958 100644
--- a/src/components/today-words/MessageList/MessageList.tsx
+++ b/src/components/today-words/MessageList/MessageList.tsx
@@ -2,6 +2,7 @@ import { useState, forwardRef, useImperativeHandle, useEffect } from 'react';
import moreIcon from '../../../assets/common/more.svg';
import type { Message } from '../../../types/today';
import MessageActionBottomSheet from './MessageActionBottomSheet';
+import { usePopupActions } from '../../../hooks/usePopupActions';
import {
MessageList as StyledMessageList,
DateGroup,
@@ -39,6 +40,7 @@ const MessageList = forwardRef(
) => {
const [selectedMessageId, setSelectedMessageId] = useState(null);
const [messages, setMessages] = useState(initialMessages);
+ const { openSnackbar } = usePopupActions();
useEffect(() => {
setMessages(initialMessages);
@@ -102,7 +104,12 @@ const MessageList = forwardRef(
if (selectedMessageId) {
// TODO: 실제 삭제 API 연동 필요
console.log(`메시지 ID ${selectedMessageId} 삭제 요청 (API 개발 대기중)`);
- alert('삭제 기능은 현재 개발 중입니다.');
+ openSnackbar({
+ message: '삭제 기능은 현재 개발 중입니다.',
+ variant: 'top',
+ isError: true,
+ onClose: () => {},
+ });
}
setSelectedMessageId(null);
};
diff --git a/src/pages/group/CreateGroup.tsx b/src/pages/group/CreateGroup.tsx
index 27be1d57..9a4722ee 100644
--- a/src/pages/group/CreateGroup.tsx
+++ b/src/pages/group/CreateGroup.tsx
@@ -74,6 +74,7 @@ const CreateGroup = () => {
const [password, setPassword] = useState('');
const [isBookSearchOpen, setIsBookSearchOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
+ const [isDateValid, setIsDateValid] = useState(true);
const handleBackClick = () => {
navigate(-1);
@@ -105,43 +106,6 @@ const CreateGroup = () => {
isPublic: !isPrivate, // isPrivate의 반대값
};
- // 날짜 검증 추가
- const today = new Date();
- const startDateObj = new Date(startDate.year, startDate.month - 1, startDate.day);
- const endDateObj = new Date(endDate.year, endDate.month - 1, endDate.day);
-
- // 시작 날짜가 오늘 이후인지 확인
- if (startDateObj <= today) {
- alert('시작 날짜는 오늘 날짜 이후여야 합니다.');
- return;
- }
-
- // 종료 날짜가 시작 날짜 이후인지 확인
- if (endDateObj <= startDateObj) {
- alert('종료 날짜는 시작 날짜 이후여야 합니다.');
- return;
- }
-
- // 요청 데이터 검증
- const validation = {
- isbn: roomData.isbn.length > 0,
- category: roomData.category.length > 0,
- roomName: roomData.roomName.length > 0,
- description: roomData.description.length > 0,
- startDate: roomData.progressStartDate.length >= 8,
- endDate: roomData.progressEndDate.length >= 8,
- recruitCount: roomData.recruitCount >= 1 && roomData.recruitCount <= 30,
- password: !isPrivate || (roomData.password !== null && /^\d{4}$/.test(roomData.password)),
- };
-
- const invalidFields = Object.entries(validation)
- .filter(([, isValid]) => !isValid)
- .map(([field]) => field);
-
- if (invalidFields.length > 0) {
- alert(`다음 필드들을 확인해주세요: ${invalidFields.join(', ')}`);
- return;
- }
// 방 생성 API 호출
const response = await createRoom(roomData);
@@ -230,6 +194,7 @@ const CreateGroup = () => {
selectedGenre !== '' &&
roomTitle.trim() !== '' &&
roomDescription.trim() !== '' &&
+ isDateValid && // 날짜 유효성 검사 추가
(!isPrivate || (password.trim() !== '' && /^\d{4}$/.test(password.trim()))) && // 비공개방인 경우 4자리 숫자 필수
!isSubmitting;
@@ -264,6 +229,7 @@ const CreateGroup = () => {
endDate={endDate}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
+ onValidationChange={setIsDateValid}
/>
diff --git a/src/pages/groupDetail/ParticipatedGroupDetail.styled.ts b/src/pages/groupDetail/ParticipatedGroupDetail.styled.ts
index 4618571c..99d47924 100644
--- a/src/pages/groupDetail/ParticipatedGroupDetail.styled.ts
+++ b/src/pages/groupDetail/ParticipatedGroupDetail.styled.ts
@@ -6,7 +6,7 @@ export const ParticipatedWrapper = styled.div`
position: relative;
flex-direction: column;
align-items: center;
- justify-content: center;
+ justify-content: flex-start;
min-width: 320px;
max-width: 767px;
min-height: 100vh;
diff --git a/src/pages/groupDetail/ParticipatedGroupDetail.tsx b/src/pages/groupDetail/ParticipatedGroupDetail.tsx
index 30bad790..b4756947 100644
--- a/src/pages/groupDetail/ParticipatedGroupDetail.tsx
+++ b/src/pages/groupDetail/ParticipatedGroupDetail.tsx
@@ -44,7 +44,7 @@ import peopleIcon from '../../assets/common/darkPeople.svg';
import styled from '@emotion/styled';
const ParticipatedGroupDetail = () => {
- const { openConfirm } = usePopupActions();
+ const { openConfirm, openSnackbar } = usePopupActions();
const navigate = useNavigate();
const { roomId } = useParams<{ roomId: string }>();
@@ -103,7 +103,12 @@ const ParticipatedGroupDetail = () => {
disc: '방을 삭제하게 되면\n독서메이트들과의 추억이 사라집니다.',
onConfirm: () => {
console.log('방 삭제 확정');
- navigate('/group');
+ openSnackbar({
+ message: '삭제 기능은 현재 개발 중입니다.',
+ variant: 'top',
+ isError: true,
+ onClose: () => {},
+ });
},
});
};
@@ -114,7 +119,12 @@ const ParticipatedGroupDetail = () => {
disc: '방을 나가시게 되면\n독서메이트들과의 추억이 사라집니다.',
onConfirm: () => {
console.log('방 나가기 확정');
- navigate('/group');
+ openSnackbar({
+ message: '나가기 기능은 현재 개발 중입니다.',
+ variant: 'top',
+ isError: true,
+ onClose: () => {},
+ });
},
});
};
diff --git a/src/pages/memory/Memory.tsx b/src/pages/memory/Memory.tsx
index 965d15f7..c11815f6 100644
--- a/src/pages/memory/Memory.tsx
+++ b/src/pages/memory/Memory.tsx
@@ -19,6 +19,7 @@ const convertPostToRecord = (post: Post): Record => {
id: post.postId.toString(),
user: post.nickName,
userPoints: 132,
+ profileImageUrl: post.profileImageUrl, // 프로필 이미지 URL 추가
content: post.content,
likeCount: post.likeCount,
commentCount: post.commentCount,
diff --git a/src/types/memory.ts b/src/types/memory.ts
index d2797bc3..f8041c31 100644
--- a/src/types/memory.ts
+++ b/src/types/memory.ts
@@ -62,6 +62,7 @@ export interface Record {
id: string;
user: string;
userPoints: number;
+ profileImageUrl: string; // 프로필 이미지 URL 추가
content: string;
likeCount: number;
commentCount: number;