{content.length} / {maxLength}
diff --git a/src/components/feed/FeedPost.tsx b/src/components/feed/FeedPost.tsx
index 8d421eb0..28d3105a 100644
--- a/src/components/feed/FeedPost.tsx
+++ b/src/components/feed/FeedPost.tsx
@@ -26,13 +26,13 @@ const BorderBottom = styled.div`
background: #1c1c1c;
`;
-const FeedPost = ({ showHeader, isLast, isMyFeed, ...postData }: FeedPostProps) => {
+const FeedPost = ({ showHeader, isLast, isMyFeed, onSaveToggle, ...postData }: FeedPostProps) => {
return (
<>
{showHeader && }
-
+
{!isLast && }
>
diff --git a/src/components/feed/UserProfileItem.tsx b/src/components/feed/UserProfileItem.tsx
index bc8694ba..50641d80 100644
--- a/src/components/feed/UserProfileItem.tsx
+++ b/src/components/feed/UserProfileItem.tsx
@@ -17,13 +17,18 @@ const UserProfileItem = ({
userId,
isLast,
type,
+ isMyself,
}: UserProfileItemProps) => {
const navigate = useNavigate();
const [followed, setFollowed] = useState(isFollowing);
const { openPopup } = usePopupStore();
const handleProfileClick = () => {
- navigate(`/otherfeed/${userId}`);
+ if (isMyself) {
+ navigate(`/myfeed/${userId}`);
+ } else {
+ navigate(`/otherfeed/${userId}`);
+ }
};
const toggleFollow = async (e: React.MouseEvent) => {
@@ -62,7 +67,7 @@ const UserProfileItem = ({
- {type === 'followlist' && (
+ {type === 'followlist' && !isMyself && (
{followed ? '띱 취소' : '띱 하기'}
diff --git a/src/components/group/CommentSection.styled.ts b/src/components/group/CommentSection.styled.ts
index 8cf2f2b7..a1e036ed 100644
--- a/src/components/group/CommentSection.styled.ts
+++ b/src/components/group/CommentSection.styled.ts
@@ -4,7 +4,7 @@ import { colors, typography } from '@/styles/global/global';
export const CommentSection = styled.section`
display: flex;
flex-direction: column;
- width: 90%;
+ width: calc(100% - 40px);
gap: 12px;
background: ${colors.darkgrey.dark};
margin: 20px 20px 0 20px;
diff --git a/src/components/group/GroupBookSection.styled.ts b/src/components/group/GroupBookSection.styled.ts
index 9663dd57..0012b6e5 100644
--- a/src/components/group/GroupBookSection.styled.ts
+++ b/src/components/group/GroupBookSection.styled.ts
@@ -5,9 +5,9 @@ export const GroupBookSection = styled.section`
display: flex;
align-items: center;
justify-content: space-between;
- width: 90%;
+ width: calc(100% - 40px);
background: ${colors.darkgrey.dark};
- margin: 0 20px 0 20px;
+ margin: 0 20px;
padding: 10px 12px;
border-radius: 12px;
cursor: pointer;
diff --git a/src/components/group/GroupBookSection.tsx b/src/components/group/GroupBookSection.tsx
index f3b9ce1e..174c83d8 100644
--- a/src/components/group/GroupBookSection.tsx
+++ b/src/components/group/GroupBookSection.tsx
@@ -18,7 +18,7 @@ const GroupBookSection = ({ title, author, onClick }: GroupBookSectionProps) =>
{title}
- {author}
+ {author} 저
diff --git a/src/components/group/HotTopicSection.styled.ts b/src/components/group/HotTopicSection.styled.ts
index 1c0c553e..a3626893 100644
--- a/src/components/group/HotTopicSection.styled.ts
+++ b/src/components/group/HotTopicSection.styled.ts
@@ -4,7 +4,7 @@ import { colors, typography } from '@/styles/global/global';
export const HotTopicSection = styled.section`
display: flex;
flex-direction: column;
- width: 90%;
+ width: calc(100% - 40px);
gap: 12px;
background: ${colors.darkgrey.dark};
margin: 20px 20px 80px 20px;
diff --git a/src/components/group/Modal.styles.ts b/src/components/group/Modal.styles.ts
index 0d1665bd..84ce7760 100644
--- a/src/components/group/Modal.styles.ts
+++ b/src/components/group/Modal.styles.ts
@@ -4,11 +4,14 @@ import styled from '@emotion/styled';
export const Overlay = styled.div<{ $whiteBg?: boolean }>`
display: flex;
justify-content: center;
- align-items: flex-start;
+ align-items: center;
position: fixed;
top: 0;
- left: 0;
- width: 100vw;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 100%;
+ max-width: 767px;
+ min-width: 320px;
height: 100vh;
background: ${({ $whiteBg }) => ($whiteBg ? 'white' : colors.black.main)};
z-index: 110;
diff --git a/src/components/group/MyGroupCard.tsx b/src/components/group/MyGroupCard.tsx
index ebbb1921..69a5c1ef 100644
--- a/src/components/group/MyGroupCard.tsx
+++ b/src/components/group/MyGroupCard.tsx
@@ -25,7 +25,7 @@ export const MyGroupCard = forwardRef((props,
{isMine ? '내 진행도' : `${group.userName}님의 진행도`}{' '}
- {group.progress}%
+ {Math.floor(group.progress || 0)}%
diff --git a/src/components/group/RecordSection.styled.ts b/src/components/group/RecordSection.styled.ts
index 34054029..3ff17e12 100644
--- a/src/components/group/RecordSection.styled.ts
+++ b/src/components/group/RecordSection.styled.ts
@@ -4,7 +4,7 @@ import { colors, semanticColors, typography } from '@/styles/global/global';
export const RecordSection = styled.section`
display: flex;
flex-direction: column;
- width: 90%;
+ width: calc(100% - 40px);
gap: 12px;
background: ${colors.darkgrey.dark};
margin: 20px 20px 0 20px;
diff --git a/src/components/group/RecordSection.tsx b/src/components/group/RecordSection.tsx
index 2ad44cd6..e3d34de7 100644
--- a/src/components/group/RecordSection.tsx
+++ b/src/components/group/RecordSection.tsx
@@ -28,7 +28,7 @@ const RecordSection = ({ currentPage, progress, onClick }: RecordSectionProps) =
현재 페이지 {currentPage}
-
{progress}
+
{Math.floor(progress)}
%
diff --git a/src/components/memory/MemoryContent/MemoryContent.styled.ts b/src/components/memory/MemoryContent/MemoryContent.styled.ts
index f3b9c167..c4211a93 100644
--- a/src/components/memory/MemoryContent/MemoryContent.styled.ts
+++ b/src/components/memory/MemoryContent/MemoryContent.styled.ts
@@ -1,5 +1,5 @@
import styled from '@emotion/styled';
-import { semanticColors, typography } from '../../../styles/global/global';
+import { semanticColors } from '../../../styles/global/global';
export const Content = styled.div`
display: flex;
@@ -23,22 +23,3 @@ export const ScrollableSection = styled.div`
display: flex;
flex-direction: column;
`;
-
-export const DevButton = styled.button`
- position: fixed;
- top: 100px;
- right: 20px;
- background: ${semanticColors.button.fill.primary};
- color: ${semanticColors.text.primary};
- border: none;
- padding: 8px 12px;
- border-radius: 4px;
- font-size: ${typography.fontSize.xs};
- cursor: pointer;
- z-index: 100;
- opacity: 0.8;
-
- &:hover {
- opacity: 1;
- }
-`;
diff --git a/src/components/memory/MemoryContent/MemoryContent.tsx b/src/components/memory/MemoryContent/MemoryContent.tsx
index ce487c8b..f62eb506 100644
--- a/src/components/memory/MemoryContent/MemoryContent.tsx
+++ b/src/components/memory/MemoryContent/MemoryContent.tsx
@@ -7,7 +7,7 @@ import RecordInfoMessage from '../RecordInfoMessage';
import EmptyRecord from '../EmptyRecord';
import RecordList from './RecordList';
import UploadProgressBar from '../UploadProgressBar/UploadProgressBar';
-import { Content, FixedSection, ScrollableSection, DevButton } from './MemoryContent.styled';
+import { Content, FixedSection, ScrollableSection } from './MemoryContent.styled';
interface MemoryContentProps {
activeTab: RecordType;
@@ -16,15 +16,12 @@ interface MemoryContentProps {
selectedSort: SortType;
records: Record[];
selectedPageRange: { start: number; end: number } | null;
- hasRecords: boolean;
showUploadProgress: boolean;
- currentUserPage: number;
onTabChange: (tab: RecordType) => void;
onFilterChange: (filter: FilterType) => void;
onSortChange: (sort: SortType) => void;
onPageRangeClear: () => void;
onPageRangeSet: (range: { start: number; end: number }) => void;
- onToggleRecords: () => void;
onUploadComplete: () => void;
}
@@ -35,21 +32,16 @@ const MemoryContent = ({
selectedSort,
records,
selectedPageRange,
- hasRecords,
showUploadProgress,
onTabChange,
onFilterChange,
onSortChange,
onPageRangeClear,
onPageRangeSet,
- onToggleRecords,
onUploadComplete,
}: MemoryContentProps) => {
return (
- {/* 개발용 버튼 */}
- {hasRecords ? '기록 숨기기' : '기록 보이기'}
-
{/* 고정 영역: 탭과 필터만 */}
@@ -81,7 +73,7 @@ const MemoryContent = ({
{records.length === 0 && }
{/* 기록 목록 */}
- {records.length > 0 && }
+ {records.length > 0 && }
);
diff --git a/src/components/memory/MemoryContent/RecordList.styled.ts b/src/components/memory/MemoryContent/RecordList.styled.ts
index 441930ae..90477709 100644
--- a/src/components/memory/MemoryContent/RecordList.styled.ts
+++ b/src/components/memory/MemoryContent/RecordList.styled.ts
@@ -3,5 +3,5 @@ import styled from '@emotion/styled';
export const RecordListContainer = styled.div`
display: flex;
flex-direction: column;
- gap: 32px;
+ gap: 40px;
`;
diff --git a/src/components/memory/MemoryContent/RecordList.tsx b/src/components/memory/MemoryContent/RecordList.tsx
index 4f1beb1b..e8f2b79e 100644
--- a/src/components/memory/MemoryContent/RecordList.tsx
+++ b/src/components/memory/MemoryContent/RecordList.tsx
@@ -4,18 +4,13 @@ import { RecordListContainer } from './RecordList.styled';
interface RecordListProps {
records: Record[];
- readingProgress: number;
}
-const RecordList = ({ records, readingProgress }: RecordListProps) => {
+const RecordList = ({ records }: RecordListProps) => {
return (
{records.map(record => (
-
+
))}
);
diff --git a/src/components/memory/PageRangeModal.styled.ts b/src/components/memory/PageRangeModal.styled.ts
index e68ad823..918f7cdc 100644
--- a/src/components/memory/PageRangeModal.styled.ts
+++ b/src/components/memory/PageRangeModal.styled.ts
@@ -153,15 +153,16 @@ export const InputGroup = styled.div`
padding: 12px 16px;
`;
-export const PageInput = styled.input<{ active?: boolean }>`
+export const PageInput = styled.input<{ active?: boolean; inputLength?: number }>`
background: none;
border: none;
outline: none;
color: ${semanticColors.text.primary};
font-size: ${typography.fontSize.base};
- width: 40px;
+ width: ${props => props.inputLength ? `${Math.max(40, props.inputLength * 12 + 10)}px` : '40px'};
text-align: center;
caret-color: ${colors.neongreen};
+ transition: width 0.2s ease;
${({ active }) =>
active &&
diff --git a/src/components/memory/PageRangeModal.tsx b/src/components/memory/PageRangeModal.tsx
index 38347d55..d95e10bb 100644
--- a/src/components/memory/PageRangeModal.tsx
+++ b/src/components/memory/PageRangeModal.tsx
@@ -98,6 +98,7 @@ const PageRangeModal = ({ onClose, onSelect }: PageRangeModalProps) => {
readOnly
onClick={() => handleInputFocus('start')}
active={activeInput === 'start'}
+ inputLength={startPage.length || 1}
/>
~
{
readOnly
onClick={() => handleInputFocus('end')}
active={activeInput === 'end'}
+ inputLength={endPage.length || 1}
/>
p
diff --git a/src/components/memory/RecordFilters/FilterButtons.styled.ts b/src/components/memory/RecordFilters/FilterButtons.styled.ts
index f00e8dad..25dc2d24 100644
--- a/src/components/memory/RecordFilters/FilterButtons.styled.ts
+++ b/src/components/memory/RecordFilters/FilterButtons.styled.ts
@@ -18,7 +18,8 @@ export const FilterButton = styled.button<{ active: boolean; $disabled?: boolean
$disabled ? semanticColors.text.tertiary : semanticColors.text.primary};
font-size: ${typography.fontSize.sm};
font-weight: ${typography.fontWeight.regular};
- cursor: pointer;
+ cursor: ${({ $disabled }) => ($disabled ? 'not-allowed' : 'pointer')};
+ pointer-events: ${({ $disabled }) => ($disabled ? 'none' : 'auto')};
transition: all 0.2s;
line-height: 24px;
display: flex;
diff --git a/src/components/memory/RecordFilters/FilterButtons.tsx b/src/components/memory/RecordFilters/FilterButtons.tsx
index d5fc4982..ef0362b4 100644
--- a/src/components/memory/RecordFilters/FilterButtons.tsx
+++ b/src/components/memory/RecordFilters/FilterButtons.tsx
@@ -59,7 +59,7 @@ const FilterButtons = ({
총평 보기
diff --git a/src/components/memory/RecordFilters/PageInputMode.styled.ts b/src/components/memory/RecordFilters/PageInputMode.styled.ts
index ffb08ffe..2d72b50b 100644
--- a/src/components/memory/RecordFilters/PageInputMode.styled.ts
+++ b/src/components/memory/RecordFilters/PageInputMode.styled.ts
@@ -38,16 +38,15 @@ export const PageInput = styled.input`
color: ${semanticColors.text.tertiary};
}
- /* 숫자 입력 스피너 제거 */
+ /* 숫자 입력 스피너 제거 (inputMode="numeric" 사용으로 불필요하지만 안전장치) */
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
- &[type='number'] {
- -moz-appearance: textfield;
- }
+ /* Firefox에서 숫자 타입 기본 스타일 제거 */
+ -moz-appearance: textfield;
`;
export const Separator = styled.span`
diff --git a/src/components/memory/RecordFilters/PageInputMode.tsx b/src/components/memory/RecordFilters/PageInputMode.tsx
index 4d9cede1..58c45242 100644
--- a/src/components/memory/RecordFilters/PageInputMode.tsx
+++ b/src/components/memory/RecordFilters/PageInputMode.tsx
@@ -36,7 +36,7 @@ const PageInputMode = ({
value={startPage}
placeholder=""
onChange={e => onInputChange('start', e.target.value)}
- type="number"
+ inputMode="numeric"
autoFocus
style={{
width: startPage.length > 0 ? `${Math.max(36, startPage.length * 12)}px` : '36px',
@@ -49,7 +49,7 @@ const PageInputMode = ({
value={endPage}
placeholder=""
onChange={e => onInputChange('end', e.target.value)}
- type="number"
+ inputMode="numeric"
style={{
width: endPage.length > 0 ? `${Math.max(36, endPage.length * 12)}px` : '36px',
}}
diff --git a/src/components/memory/RecordFilters/RecordFilters.tsx b/src/components/memory/RecordFilters/RecordFilters.tsx
index a69e1271..a713c553 100644
--- a/src/components/memory/RecordFilters/RecordFilters.tsx
+++ b/src/components/memory/RecordFilters/RecordFilters.tsx
@@ -46,13 +46,13 @@ const RecordFilters = ({
};
const handleInputChange = (type: 'start' | 'end', value: string) => {
- // 숫자만 입력 허용
- const numericValue = value.replace(/[^0-9]/g, '');
-
- if (type === 'start') {
- setStartPage(numericValue);
- } else {
- setEndPage(numericValue);
+ // 숫자만 입력 허용 (빈 문자열 또는 숫자)
+ if (value === '' || /^\d+$/.test(value)) {
+ if (type === 'start') {
+ setStartPage(value);
+ } else {
+ setEndPage(value);
+ }
}
};
@@ -65,7 +65,8 @@ const RecordFilters = ({
const start = parseInt(startPage);
const end = parseInt(endPage);
- if (start && end && start <= end) {
+ // NaN 체크 포함한 유효성 검사
+ if (!isNaN(start) && !isNaN(end) && start > 0 && end > 0 && start <= end) {
// 페이지 범위를 상위 컴포넌트에 전달
if (onPageRangeSet) {
onPageRangeSet({ start, end });
@@ -77,6 +78,15 @@ const RecordFilters = ({
}
};
+ const handleCancel = () => {
+ setShowInputMode(false);
+ setStartPage('');
+ setEndPage('');
+ if (onPageRangeClear) {
+ onPageRangeClear();
+ }
+ };
+
const handleOverallFilterClick = () => {
onFilterChange('overall');
};
@@ -90,36 +100,60 @@ const RecordFilters = ({
setIsDropdownOpen(false);
};
- const isValid = Boolean(startPage && endPage && parseInt(startPage) <= parseInt(endPage));
+ const isValid = Boolean(
+ startPage &&
+ endPage &&
+ !isNaN(parseInt(startPage)) &&
+ !isNaN(parseInt(endPage)) &&
+ parseInt(startPage) > 0 &&
+ parseInt(endPage) > 0 &&
+ parseInt(startPage) <= parseInt(endPage)
+ );
const hasAnyInput = startPage.length > 0 || endPage.length > 0;
const isPageInputMode = showInputMode && activeFilter === 'page' && !selectedPageRange;
return (
-
- {isPageInputMode ? (
-
- ) : (
-
+ {isPageInputMode && (
+
)}
-
+
+ {isPageInputMode ? (
+
+ ) : (
+
+ )}
+
+ >
);
};
diff --git a/src/components/memory/RecordInfoMessage.tsx b/src/components/memory/RecordInfoMessage.tsx
index 100f98f2..65bb612a 100644
--- a/src/components/memory/RecordInfoMessage.tsx
+++ b/src/components/memory/RecordInfoMessage.tsx
@@ -7,7 +7,7 @@ const RecordInfoMessage = () => {
- 내 진행도에 따라 일부 댓글은 블러처리됩니다.
+ 내 진행도에 따라 일부 기록은 블러처리됩니다.
);
};
diff --git a/src/components/memory/RecordItem/PollRecord.tsx b/src/components/memory/RecordItem/PollRecord.tsx
index 14fb76d0..a21bc333 100644
--- a/src/components/memory/RecordItem/PollRecord.tsx
+++ b/src/components/memory/RecordItem/PollRecord.tsx
@@ -20,10 +20,11 @@ interface PollRecordProps {
content: string;
pollOptions: PollOption[];
postId: number; // 투표 API 호출에 필요한 postId
+ shouldBlur?: boolean; // 블라인드 처리 여부
onVoteUpdate?: (updatedOptions: PollOption[]) => void; // 투표 결과 업데이트 콜백
}
-const PollRecord = ({ content, pollOptions, postId, onVoteUpdate }: PollRecordProps) => {
+const PollRecord = ({ content, pollOptions, postId, shouldBlur = false, onVoteUpdate }: PollRecordProps) => {
const [animate, setAnimate] = useState(false);
const [currentOptions, setCurrentOptions] = useState(pollOptions);
const [isVoting, setIsVoting] = useState(false);
@@ -67,7 +68,7 @@ const PollRecord = ({ content, pollOptions, postId, onVoteUpdate }: PollRecordPr
// 투표 옵션 클릭 핸들러
const handleOptionClick = async (option: PollOption) => {
- if (isVoting || !roomId) return;
+ if (isVoting || !roomId || shouldBlur) return;
setIsVoting(true);
@@ -148,10 +149,11 @@ const PollRecord = ({ content, pollOptions, postId, onVoteUpdate }: PollRecordPr
handleOptionClick(option)}
+ onClick={shouldBlur ? undefined : () => handleOptionClick(option)}
style={{
- cursor: isVoting ? 'not-allowed' : 'pointer',
- opacity: isVoting ? 0.7 : 1
+ cursor: shouldBlur ? 'default' : (isVoting ? 'not-allowed' : 'pointer'),
+ opacity: shouldBlur ? 1 : (isVoting ? 0.7 : 1),
+ pointerEvents: shouldBlur ? 'none' : 'auto'
}}
>
diff --git a/src/components/memory/RecordItem/RecordItem.tsx b/src/components/memory/RecordItem/RecordItem.tsx
index cea788b9..71368449 100644
--- a/src/components/memory/RecordItem/RecordItem.tsx
+++ b/src/components/memory/RecordItem/RecordItem.tsx
@@ -34,7 +34,7 @@ interface RecordItemProps {
const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => {
const navigate = useNavigate();
const { roomId } = useParams<{ roomId: string }>();
- const { openMoreMenu, openConfirm, openSnackbar } = usePopupActions();
+ const { openMoreMenu, openConfirm, openSnackbar, closePopup } = usePopupActions();
const {
id,
@@ -53,7 +53,7 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => {
// 좋아요 상태 관리 - record 객체에서 isLiked 속성 가져오기
const [isLiked, setIsLiked] = useState(record.isLiked || false);
const [currentLikeCount, setCurrentLikeCount] = useState(likeCount);
-
+
// 전역 댓글 바텀시트
const { openCommentBottomSheet } = useCommentBottomSheetStore();
@@ -129,14 +129,9 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => {
};
const handleEdit = useCallback(() => {
- const currentRoomId = roomId || '1';
-
- if (type === 'poll') {
- navigate(`/memory/poll/edit/${currentRoomId}/${record.id}`);
- } else {
- navigate(`/memory/record/edit/${currentRoomId}/${record.id}`);
- }
- }, [roomId, type, record.id, navigate]);
+ // API 개발 전까지 수정 기능 비활성화
+ console.log('수정 기능은 개발 중입니다.');
+ }, []);
const handleDelete = useCallback(async () => {
const currentRoomId = roomId || '1';
@@ -204,8 +199,11 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => {
try {
const response = await pinRecordToFeed(parseInt(currentRoomId), recordId);
-
+
if (response.isSuccess) {
+ // 팝업 먼저 닫기
+ closePopup();
+
// 피드 작성 페이지로 이동하면서 데이터 전달
navigate('/feed/write', {
state: {
@@ -217,12 +215,12 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => {
recordContent: content,
roomId: currentRoomId,
recordId: record.id,
- }
- }
+ },
+ },
});
} else {
let errorMessage = '핀하기에 실패했습니다.';
-
+
if (response.code === 130000) {
errorMessage = '존재하지 않는 기록입니다.';
} else if (response.code === 130003) {
@@ -233,6 +231,9 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => {
errorMessage = '존재하지 않는 책입니다.';
}
+ // 실패한 경우에도 팝업 닫기
+ closePopup();
+
openSnackbar({
message: errorMessage,
variant: 'top',
@@ -241,22 +242,27 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => {
}
} catch (error) {
console.error('핀하기 API 호출 실패:', error);
+
+ // 네트워크 오류 시에도 팝업 닫기
+ closePopup();
+
openSnackbar({
message: '네트워크 오류가 발생했습니다. 다시 시도해주세요.',
variant: 'top',
onClose: () => {},
});
}
- }, [roomId, record.id, content, navigate, openSnackbar]);
+ }, [roomId, record.id, content, navigate, openSnackbar, closePopup]);
// 핀하기 확인 팝업 핸들러
const handlePinConfirm = useCallback(() => {
openConfirm({
- title: '기록을 피드에 핀하시겠어요?',
- disc: '기록의 내용으로 피드 글 작성 페이지가 열립니다.',
+ title: '이 기록을 피드에 핀할까요?',
+ disc: '핀하면 내 피드에 글을 옮길 수 있어요.',
onConfirm: handlePinRecord,
+ onClose: closePopup, // "아니오" 버튼 클릭 시 팝업 닫기
});
- }, [openConfirm, handlePinRecord]);
+ }, [openConfirm, handlePinRecord, closePopup]);
// 댓글 버튼 클릭 핸들러
const handleCommentClick = useCallback(() => {
@@ -266,8 +272,6 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => {
// 길게 누르기 이벤트 핸들러
const handleTouchStart = useCallback(
(e: React.TouchEvent) => {
- if (isMyRecord) return;
-
setIsPressed(true);
hasTriggeredLongPress.current = false;
pressStartPos.current = {
@@ -279,12 +283,32 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => {
hasTriggeredLongPress.current = true;
setIsPressed(false);
- openMoreMenu({
- onReport: handleReport,
- });
+ 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],
+ [
+ isMyRecord,
+ openMoreMenu,
+ handleReport,
+ handleEdit,
+ handleDeleteConfirm,
+ handlePinConfirm,
+ closePopup,
+ ],
);
const handleTouchMove = useCallback((e: React.TouchEvent) => {
@@ -315,17 +339,8 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => {
hasTriggeredLongPress.current = false;
return;
}
-
- if (isMyRecord) {
- openMoreMenu({
- onEdit: handleEdit,
- onDelete: handleDeleteConfirm,
- onPin: handlePinConfirm,
- type: 'post', // 중요: post 타입으로 설정해야 핀하기 버튼이 표시됨
- isWriter: true, // 내 기록임을 명시
- });
- }
- }, [isMyRecord, openMoreMenu, handleEdit, handleDeleteConfirm, handlePinConfirm]);
+ // 모든 기록(내 기록 포함)은 이제 길게 누르기로만 더보기 메뉴 표시
+ }, []);
return (
{
{type === 'text' ? (
) : (
- {
+ shouldBlur={shouldBlur}
+ onVoteUpdate={updatedOptions => {
// TODO: 부모 컴포넌트로 투표 결과 업데이트 전달
console.log('투표 결과 업데이트:', updatedOptions);
}}
@@ -366,35 +382,62 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => {
- {
- e.stopPropagation();
- handleLikeClick();
- }}>
+ {
+ e.stopPropagation();
+ handleLikeClick();
+ }
+ }
+ style={{
+ cursor: shouldBlur ? 'default' : 'pointer',
+ pointerEvents: shouldBlur ? 'none' : 'auto',
+ }}
+ >
{currentLikeCount}
- {
- e.stopPropagation();
- handleCommentClick();
- }}>
+ {
+ e.stopPropagation();
+ handleCommentClick();
+ }
+ }
+ style={{
+ cursor: shouldBlur ? 'default' : 'pointer',
+ pointerEvents: shouldBlur ? 'none' : 'auto',
+ }}
+ >
{commentCount}
{isMyRecord && (
- {
- e.stopPropagation(); // 이벤트 버블링 방지
- handlePinConfirm();
+ {
+ e.stopPropagation(); // 이벤트 버블링 방지
+ handlePinConfirm();
+ }
+ }
+ style={{
+ cursor: shouldBlur ? 'default' : 'pointer',
+ pointerEvents: shouldBlur ? 'none' : 'auto',
}}
>
)}
-
);
};
diff --git a/src/components/recordwrite/PageRangeSection.styled.ts b/src/components/recordwrite/PageRangeSection.styled.ts
index 0d292b11..564078e0 100644
--- a/src/components/recordwrite/PageRangeSection.styled.ts
+++ b/src/components/recordwrite/PageRangeSection.styled.ts
@@ -1,5 +1,5 @@
import styled from '@emotion/styled';
-import { typography, semanticColors } from '../../styles/global/global';
+import { colors, typography, semanticColors } from '../../styles/global/global';
export const Section = styled.div`
display: flex;
@@ -41,7 +41,7 @@ export const InputWrapper = styled.div`
font-family: ${typography.fontFamily.primary};
`;
-export const PageInput = styled.input`
+export const PageInput = styled.input<{ inputLength?: number }>`
background: none;
border: none;
outline: none;
@@ -49,13 +49,25 @@ export const PageInput = styled.input`
font-size: ${typography.fontSize.sm};
font-weight: ${typography.fontWeight.regular};
font-family: ${typography.fontFamily.primary};
- width: 30px;
+ width: ${props => (props.inputLength ? `${Math.max(30, props.inputLength * 8 + 10)}px` : '30px')};
padding: 0;
margin: 0;
+ caret-color: ${colors.white};
+ transition: width 0.2s ease;
&::placeholder {
color: ${semanticColors.text.ghost};
}
+
+ /* 숫자 입력 스피너 제거 */
+ &::-webkit-outer-spin-button,
+ &::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+
+ /* Firefox에서 기본 스타일 제거 */
+ -moz-appearance: textfield;
`;
export const PageSuffix = styled.span`
@@ -124,6 +136,7 @@ export const InfoIcon = styled.div`
justify-content: center;
width: 20px;
height: 20px;
+ cursor: pointer;
img {
width: 20px;
@@ -164,3 +177,52 @@ export const ToggleSlider = styled.div<{ active: boolean; disabled?: boolean }>`
transition: left 0.3s;
opacity: ${props => (props.disabled ? 0.7 : 1)};
`;
+
+export const TooltipContainer = styled.div`
+ position: relative;
+ width: 100%;
+`;
+
+export const Tooltip = styled.div<{ variant: 'red' | 'green' }>`
+ position: absolute;
+ top: 33px;
+ right: 0;
+ left: 0;
+ background-color: ${props => (props.variant === 'red' ? '#3d3d3d' : '#3d3d3d')};
+ border-radius: 12px;
+ padding: 21px 12px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ z-index: 10;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+`;
+
+export const TooltipText = styled.span<{ variant: 'red' | 'green' }>`
+ color: ${props => (props.variant === 'red' ? '#FF9496' : semanticColors.text.point.green)};
+ font-size: ${typography.fontSize.xs};
+ font-weight: ${typography.fontWeight.medium};
+ flex: 1;
+`;
+
+export const TooltipCloseButton = styled.button`
+ background: none;
+ border: none;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+`;
+
+export const TooltipArrow = styled.div<{ variant: 'red' | 'green' }>`
+ position: absolute;
+ bottom: 60px;
+ right: 61px;
+ width: 12px;
+ height: 12px;
+ background-color: #3d3d3d;
+ transform: rotate(45deg);
+ z-index: 9;
+`;
diff --git a/src/components/recordwrite/PageRangeSection.tsx b/src/components/recordwrite/PageRangeSection.tsx
index 91082ea0..fd8d7692 100644
--- a/src/components/recordwrite/PageRangeSection.tsx
+++ b/src/components/recordwrite/PageRangeSection.tsx
@@ -16,9 +16,15 @@ import {
ToggleLabel,
ToggleSwitch,
ToggleSlider,
+ TooltipContainer,
+ Tooltip,
+ TooltipText,
+ TooltipCloseButton,
+ TooltipArrow,
} from './PageRangeSection.styled';
import closeIcon from '../../assets/common/delete.svg';
import infoIcon from '../../assets/common/infoIcon.svg';
+import recordcloseIcon from '../../assets/memory/record-x.svg';
interface PageRangeSectionProps {
pageRange: string;
@@ -41,6 +47,8 @@ const PageRangeSection = ({
readingProgress,
}: PageRangeSectionProps) => {
const [hasError, setHasError] = useState(false);
+ const [showRedTooltip, setShowRedTooltip] = useState(false);
+ const [showGreenTooltip, setShowGreenTooltip] = useState(false);
// 80% 이상일 때만 총평 활성화
const canUseOverall = readingProgress >= 80;
@@ -48,14 +56,15 @@ const PageRangeSection = ({
const handleInputChange = (e: React.ChangeEvent) => {
const value = e.target.value;
- // 숫자만 입력 허용
+ // 숫자만 입력 허용 (빈 문자열 또는 양수)
if (value === '' || /^\d+$/.test(value)) {
onPageRangeChange(value);
// 전체 페이지 수를 초과하면 에러 상태로 변경
if (value !== '') {
const page = parseInt(value);
- setHasError(page > totalPages);
+ // NaN 체크도 포함하여 안전성 확보
+ setHasError(isNaN(page) || page <= 0 || page > totalPages);
} else {
setHasError(false);
}
@@ -70,8 +79,22 @@ const PageRangeSection = ({
const handleToggleClick = () => {
if (canUseOverall) {
onOverallToggle();
+ } else {
+ // 80% 미만이면 빨간 툴팁 표시
+ setShowRedTooltip(true);
+ setShowGreenTooltip(false);
}
- // 80% 미만이면 아무것도 하지 않음
+ };
+
+ const handleInfoClick = () => {
+ // i 아이콘 클릭 시 초록 툴팁 표시
+ setShowGreenTooltip(true);
+ setShowRedTooltip(false);
+ };
+
+ const handleCloseTooltip = () => {
+ setShowRedTooltip(false);
+ setShowGreenTooltip(false);
};
return (
@@ -88,6 +111,7 @@ const PageRangeSection = ({
value={pageRange}
onChange={handleInputChange}
inputMode="numeric"
+ inputLength={pageRange.length || lastRecordedPage.toString().length}
/>
/{totalPages}p
@@ -101,21 +125,45 @@ const PageRangeSection = ({
{!isOverallEnabled && hasError && 전체페이지를 초과할 수 없어요.}
-
-
-
-
-
- 총평
-
-
-
-
-
+
+ {showRedTooltip && (
+
+
+ 독서 진행도 80%를 달성해야 총평을 작성할 수 있어요.
+
+
+
+
+
+
+ )}
+ {showGreenTooltip && (
+
+
+ 독서 진행도 80%를 달성해야 총평을 작성할 수 있어요.
+
+
+
+
+
+
+ )}
+
+
+
+
+
+ 총평
+
+
+
+
+
+
);
};
diff --git a/src/pages/feed/FollowerListPage.tsx b/src/pages/feed/FollowerListPage.tsx
index 69931a95..c4129f26 100644
--- a/src/pages/feed/FollowerListPage.tsx
+++ b/src/pages/feed/FollowerListPage.tsx
@@ -135,6 +135,7 @@ const FollowerListPage = () => {
type={type as UserProfileType}
isFollowing={user.isFollowing}
isLast={index === userList.length - 1}
+ isMyself={user.isMyself}
/>
))}
@@ -156,7 +157,9 @@ const Wrapper = styled.div`
const TotalBar = styled.div`
position: fixed;
top: 0;
- width: 727px;
+ width: 94.8%;
+ max-width: 727px;
+ min-width: 320px;
padding: 76px 0px 4px 0px;
border-bottom: 1px solid var(--color-darkgrey-dark);
background-color: var(--color-black-main);
diff --git a/src/pages/group/CreateGroup.tsx b/src/pages/group/CreateGroup.tsx
index e405b9ae..27be1d57 100644
--- a/src/pages/group/CreateGroup.tsx
+++ b/src/pages/group/CreateGroup.tsx
@@ -238,7 +238,7 @@ const CreateGroup = () => {
}
title="모임 만들기"
- rightButton={isSubmitting ? '생성 중...' : '완료'}
+ rightButton="완료"
onLeftClick={handleBackClick}
onRightClick={handleCompleteClick}
isNextActive={isFormValid}
diff --git a/src/pages/groupDetail/GroupDetail.styled.ts b/src/pages/groupDetail/GroupDetail.styled.ts
index f80a5cff..7aa58527 100644
--- a/src/pages/groupDetail/GroupDetail.styled.ts
+++ b/src/pages/groupDetail/GroupDetail.styled.ts
@@ -135,8 +135,25 @@ export const Tag = styled.div`
}
`;
-export const TagGenre = styled.span`
- color: ${colors.character.orange};
+const getGenreColor = (genre?: string): string => {
+ switch (genre) {
+ case '문학':
+ return colors.character.mint;
+ case '과학·IT':
+ return colors.character.lavender;
+ case '예술':
+ return colors.character.pink;
+ case '사회과학':
+ return colors.character.orange;
+ case '인문학':
+ return colors.character.skyblue;
+ default:
+ return colors.character.orange;
+ }
+};
+
+export const TagGenre = styled.span<{ genre?: string }>`
+ color: ${({ genre }) => getGenreColor(genre)};
`;
export const BookSection = styled.section`
diff --git a/src/pages/groupDetail/ParticipatedGroupDetail.styled.ts b/src/pages/groupDetail/ParticipatedGroupDetail.styled.ts
index 055303d8..4618571c 100644
--- a/src/pages/groupDetail/ParticipatedGroupDetail.styled.ts
+++ b/src/pages/groupDetail/ParticipatedGroupDetail.styled.ts
@@ -9,7 +9,7 @@ export const ParticipatedWrapper = styled.div`
justify-content: center;
min-width: 320px;
max-width: 767px;
- height: 100%;
+ min-height: 100vh;
margin: 0 auto;
background-color: ${colors.black.main};
overflow: hidden;
diff --git a/src/pages/groupDetail/ParticipatedGroupDetail.tsx b/src/pages/groupDetail/ParticipatedGroupDetail.tsx
index e4aa06e6..30bad790 100644
--- a/src/pages/groupDetail/ParticipatedGroupDetail.tsx
+++ b/src/pages/groupDetail/ParticipatedGroupDetail.tsx
@@ -141,7 +141,7 @@ const ParticipatedGroupDetail = () => {
const handleBookSectionClick = () => {
if (roomData?.data.isbn) {
- navigate(`/book/${roomData.data.isbn}`);
+ navigate(`/search/book/${roomData.data.isbn}`);
}
};
@@ -234,7 +234,7 @@ const ParticipatedGroupDetail = () => {
- 장르 {data.category}
+ 장르 {data.category}
diff --git a/src/pages/memory/Memory.tsx b/src/pages/memory/Memory.tsx
index 083ae705..965d15f7 100644
--- a/src/pages/memory/Memory.tsx
+++ b/src/pages/memory/Memory.tsx
@@ -29,6 +29,7 @@ const convertPostToRecord = (post: Post): Record => {
pageRange: post.isOverview ? undefined : post.page.toString(),
isWriter: post.isWriter,
isLiked: post.isLiked,
+ isLocked: post.isLocked, // 블러 처리 여부 추가
pollOptions: post.voteItems.map((item, index) => ({
id: item.voteItemId.toString(),
text: item.itemName,
@@ -61,12 +62,13 @@ const Memory = () => {
// 업로드 프로그레스 상태
const [showUploadProgress, setShowUploadProgress] = useState(false);
- // 개발용 상태 - 기록 유무 전환
- const [hasRecords, setHasRecords] = useState(true);
-
// 기록 데이터
const [myRecords, setMyRecords] = useState([]);
const [groupRecords, setGroupRecords] = useState([]);
+
+ // API에서 받은 페이지 정보
+ const [totalPages, setTotalPages] = useState(0);
+ const [currentUserPage, setCurrentUserPage] = useState(0);
// API 데이터 로드 함수
const loadMemoryPosts = useCallback(async () => {
@@ -114,6 +116,14 @@ const Memory = () => {
}
setIsOverviewEnabled(response.data.isOverviewEnabled);
+
+ // 페이지 정보 설정 (API에서 제공되면)
+ if (response.data.totalPages !== undefined) {
+ setTotalPages(response.data.totalPages);
+ }
+ if (response.data.currentUserPage !== undefined) {
+ setCurrentUserPage(response.data.currentUserPage);
+ }
} else {
setError(response.message);
}
@@ -156,11 +166,8 @@ const Memory = () => {
// 현재 탭에 따른 기록 목록 결정
const currentRecords = useMemo(() => {
- if (!hasRecords) {
- return [];
- }
return activeTab === 'group' ? groupRecords : myRecords;
- }, [activeTab, hasRecords, groupRecords, myRecords]);
+ }, [activeTab, groupRecords, myRecords]);
// 정렬된 기록 목록
const sortedRecords = useMemo(() => {
@@ -189,7 +196,7 @@ const Memory = () => {
// 이벤트 핸들러들
const handleBackClick = useCallback(() => {
if (roomId) {
- navigate(`/rooms/${roomId}`);
+ navigate(`/group/detail/joined/${roomId}`);
} else {
navigate('/group');
}
@@ -228,17 +235,16 @@ const Memory = () => {
setActiveFilter('page');
}, []);
- const handleToggleRecords = useCallback(() => {
- setHasRecords(!hasRecords);
- }, [hasRecords]);
-
const handleUploadComplete = useCallback(() => {
setShowUploadProgress(false);
}, []);
- // 독서 진행률 계산
- const readingProgress = isOverviewEnabled ? 85 : 70;
- const currentUserPage = 350; // 임시로 350으로 설정 (나중에 API에서 가져올 것)
+ // 독서 진행률 계산 (전체 페이지 대비 현재 페이지 퍼센트)
+ const readingProgress = totalPages > 0 ? Math.round((currentUserPage / totalPages) * 100) : 0;
+
+ // 총평 활성화 상태를 읽기 진행률에 따라 표시용으로 활용
+ const overviewStatus = isOverviewEnabled ? '총평 활성화' : '총평 비활성화';
+ console.log('📊 현재 상태:', overviewStatus, `진행률: ${readingProgress}%`);
// 에러 상태 렌더링
if (error) {
@@ -272,15 +278,12 @@ const Memory = () => {
selectedSort={selectedSort}
records={filteredRecords}
selectedPageRange={selectedPageRange}
- hasRecords={hasRecords}
showUploadProgress={showUploadProgress}
- currentUserPage={currentUserPage}
onTabChange={handleTabChange}
onFilterChange={handleFilterChange}
onSortChange={handleSortChange}
onPageRangeClear={handlePageRangeClear}
onPageRangeSet={handlePageRangeSet}
- onToggleRecords={handleToggleRecords}
onUploadComplete={handleUploadComplete}
/>
diff --git a/src/pages/mypage/SavePage.tsx b/src/pages/mypage/SavePage.tsx
index 54ac2391..12530160 100644
--- a/src/pages/mypage/SavePage.tsx
+++ b/src/pages/mypage/SavePage.tsx
@@ -11,6 +11,7 @@ import { colors, typography } from '@/styles/global/global';
import { getSavedBooksInMy, type SavedBookInMy } from '@/api/books/getSavedBooksInMy';
import { getSavedFeedsInMy, type SavedFeedInMy } from '@/api/feeds/getSavedFeedsInMy';
import { postSaveBook } from '@/api/books/postSaveBook';
+
import LoadingSpinner from '@/components/common/LoadingSpinner';
const tabs = ['피드', '책'];
@@ -38,7 +39,17 @@ const SavePage = () => {
navigate('/mypage');
};
- // 저장된 피드 로드
+ // 저장된 책 목록 로드 함수
+ const loadSavedBooks = useCallback(async () => {
+ try {
+ const response = await getSavedBooksInMy();
+ setSavedBooks(response.data.bookList);
+ } catch (error) {
+ console.error('저장된 책 목록 로드 실패:', error);
+ }
+ }, []);
+
+ // 저장된 피드 목록 로드 함수
const loadSavedFeeds = useCallback(async (cursor: string | null = null) => {
try {
setFeedLoading(true);
@@ -61,7 +72,7 @@ const SavePage = () => {
}
}, []);
- // 페이지 진입 시 모든 데이터 로드
+ // 페이지 진입 시 모든 데이터 로드 (한 번만 실행)
useEffect(() => {
const loadAllData = async () => {
try {
@@ -88,7 +99,7 @@ const SavePage = () => {
};
loadAllData();
- }, []); // 한 번만 실행
+ }, []); // 빈 의존성 배열로 변경
// Intersection Observer 설정 (피드)
useEffect(() => {
@@ -122,10 +133,15 @@ const SavePage = () => {
const newSaveState = !currentBook.isSaved;
await postSaveBook(isbn, newSaveState);
- // 로컬 상태 업데이트
- setSavedBooks(prev =>
- prev.map(book => (book.isbn === isbn ? { ...book, isSaved: newSaveState } : book)),
- );
+ // 저장 취소인 경우 저장된 책 목록을 다시 불러옴
+ if (!newSaveState) {
+ await loadSavedBooks();
+ } else {
+ // 저장인 경우 로컬 상태만 업데이트
+ setSavedBooks(prev =>
+ prev.map(book => (book.isbn === isbn ? { ...book, isSaved: newSaveState } : book)),
+ );
+ }
console.log('저장 토글:', isbn, newSaveState);
} catch (error) {
@@ -133,6 +149,19 @@ const SavePage = () => {
}
};
+ // 피드 저장 토글 처리
+ const handleFeedSaveToggle = async (feedId: number, newSaveState: boolean) => {
+ try {
+ if (!newSaveState) {
+ // 저장 취소인 경우 리스트에서 제거
+ setSavedFeeds(prev => prev.filter(feed => feed.feedId !== feedId));
+ console.log('피드 저장 취소 완료:', feedId);
+ }
+ } catch (error) {
+ console.error('피드 저장 상태 변경 실패:', error);
+ }
+ };
+
return (
{
showHeader={true}
isMyFeed={false}
isLast={index === savedFeeds.length - 1}
+ onSaveToggle={handleFeedSaveToggle}
{...feed}
/>
))}
@@ -258,8 +288,9 @@ const EmptyState = styled.div`
const BookList = styled.div`
display: flex;
flex-direction: column;
+ width: 100%;
min-width: 320px;
- max-width: 540px;
+ max-width: 767px;
padding-top: 32px;
margin: 0 auto;
width: 100%;
diff --git a/src/pages/post/CreatePost.tsx b/src/pages/post/CreatePost.tsx
index 5948c444..10c6bc84 100644
--- a/src/pages/post/CreatePost.tsx
+++ b/src/pages/post/CreatePost.tsx
@@ -171,7 +171,7 @@ const CreatePost = () => {
}
title="새 글"
- rightButton={loading ? '작성 중...' : '완료'}
+ rightButton="완료"
onLeftClick={handleBackClick}
onRightClick={handleCompleteClick}
isNextActive={isFormValid && !loading}
@@ -189,7 +189,7 @@ const CreatePost = () => {
diff --git a/src/pages/post/UpdatePost.tsx b/src/pages/post/UpdatePost.tsx
index 2991a416..597b4d16 100644
--- a/src/pages/post/UpdatePost.tsx
+++ b/src/pages/post/UpdatePost.tsx
@@ -64,7 +64,7 @@ const UpdatePost = () => {
id: 0,
title: data.bookTitle,
author: data.bookAuthor,
- cover: '',
+ cover: data.bookImageUrl,
isbn: data.isbn,
});
@@ -152,7 +152,7 @@ const UpdatePost = () => {
}
title="글 수정"
- rightButton={updateLoading ? '수정 중...' : '완료'}
+ rightButton="완료"
onLeftClick={handleBackClick}
onRightClick={handleCompleteClick}
isNextActive={isFormValid && !updateLoading}
diff --git a/src/pages/searchBook/SearchBook.tsx b/src/pages/searchBook/SearchBook.tsx
index f6b8e42a..ff362416 100644
--- a/src/pages/searchBook/SearchBook.tsx
+++ b/src/pages/searchBook/SearchBook.tsx
@@ -238,7 +238,7 @@ const SearchBook = () => {
if (isLoading || error || !bookDetail) {
if (isLoading) {
- return ;
+ return ;
}
return (
diff --git a/src/pages/today-words/TodayWords.tsx b/src/pages/today-words/TodayWords.tsx
index f80932d7..193cff0a 100644
--- a/src/pages/today-words/TodayWords.tsx
+++ b/src/pages/today-words/TodayWords.tsx
@@ -30,18 +30,32 @@ const TodayWords = () => {
// 하루 5개 제한 관련
const DAILY_LIMIT = 5;
+ // 오늘 날짜를 여러 포맷으로 생성하는 함수
+ const getTodayDateStrings = useCallback(() => {
+ const today = new Date();
+ const year = today.getFullYear();
+ const month = String(today.getMonth() + 1).padStart(2, '0');
+ const day = String(today.getDate()).padStart(2, '0');
+
+ return [
+ `${year}.${month}.${day}`, // 2024.01.15
+ `${year}년 ${month}월 ${day}일`, // 2024년 01월 15일
+ `${year}-${month}-${day}`, // 2024-01-15
+ `${month}/${day}/${year}`, // 01/15/2024
+ ];
+ }, []);
+
// 오늘 작성한 내 메시지 개수 계산
const getTodayMyMessageCount = useCallback(() => {
- const today = new Date().toLocaleDateString('ko-KR', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- }).replace(/\. /g, '.').replace(/\.$/, '');
+ const todayFormats = getTodayDateStrings();
- return messages.filter(message =>
- message.isWriter === true && message.timestamp === today
- ).length;
- }, [messages]);
+ return messages.filter(message => {
+ if (!message.isWriter) return false;
+
+ // 여러 날짜 포맷과 비교
+ return todayFormats.includes(message.timestamp);
+ }).length;
+ }, [messages, getTodayDateStrings]);
const todayMyMessageCount = getTodayMyMessageCount();
@@ -52,8 +66,7 @@ const TodayWords = () => {
// API 데이터를 Message 타입으로 변환하는 함수
const convertToMessage = (item: TodayCommentItem): Message => {
- // 네트워크 응답에서 postDate가 "1일 전" 형태의 문자열로 오는 것으로 보임
- // 따라서 postDate를 그대로 timeAgo로 사용
+ // 서버에서 받아오는 postDate를 그대로 사용 (이미 날짜 기반으로 계산된 값)
const timeAgo = item.postDate || '방금 전';
// createdAt은 현재 시간으로 설정 (정확한 시간이 필요하다면 다른 API 필드 사용)
@@ -99,6 +112,13 @@ const TodayWords = () => {
setNextCursor(response.data.nextCursor);
setIsLast(response.data.isLast);
setHasInitiallyLoaded(true);
+
+ // 초기 로딩 시 스크롤을 맨 아래로 이동
+ if (isRefresh) {
+ setTimeout(() => {
+ window.scrollTo({ top: document.body.scrollHeight, behavior: 'auto' });
+ }, 100);
+ }
} else {
openSnackbar({
message: response.message || '오늘의 한마디 목록을 불러오는데 실패했습니다.',
@@ -139,21 +159,21 @@ const TodayWords = () => {
setIsLoading(false);
setIsLoadingMore(false);
}
- }, [roomId]);
+ }, [roomId, convertToMessage, openSnackbar]);
// 더 많은 메시지 로드
const loadMoreMessages = useCallback(() => {
if (!isLoadingMore && !isLast && nextCursor && roomId) {
loadMessages(nextCursor);
}
- }, [isLoadingMore, isLast, nextCursor, roomId]);
+ }, [isLoadingMore, isLast, nextCursor, roomId, loadMessages]);
// 컴포넌트 마운트 시 초기 데이터 로드
useEffect(() => {
if (roomId && !hasInitiallyLoaded) {
loadMessages(undefined, true);
}
- }, [roomId, hasInitiallyLoaded]);
+ }, [roomId, hasInitiallyLoaded, loadMessages]);
// 무한 스크롤 처리
useEffect(() => {
@@ -188,6 +208,7 @@ const TodayWords = () => {
openSnackbar({
message: '오늘의 한마디는 하루에 다섯번까지 작성할 수 있어요',
variant: 'top',
+ isError: true,
onClose: () => {},
});
return;
@@ -209,14 +230,16 @@ const TodayWords = () => {
setIsLast(false);
setHasInitiallyLoaded(false);
- // 5개 도달 시 흰색 토스트, 아니면 일반 성공 메시지
+ // 첫 번째 한마디 작성 시만 성공 메시지, 5개 도달 시 제한 메시지
if (todayMyMessageCount + 1 >= DAILY_LIMIT) {
openSnackbar({
message: '오늘의 한마디는 하루에 다섯번까지 작성할 수 있어요',
variant: 'top',
+ isError: true,
onClose: () => {},
});
- } else {
+ } else if (todayMyMessageCount === 0) {
+ // 첫 번째 한마디 작성 시만 토스트 메시지 표시
openSnackbar({
message: '오늘의 한마디가 작성되었습니다.',
variant: 'top',
diff --git a/src/stores/usePopupStore.ts b/src/stores/usePopupStore.ts
index 258f4caa..acd104e6 100644
--- a/src/stores/usePopupStore.ts
+++ b/src/stores/usePopupStore.ts
@@ -30,6 +30,7 @@ export interface SnackbarProps {
message: string;
actionText?: string;
variant: 'top' | 'bottom';
+ isError?: boolean;
onActionClick?: () => void;
onClose: () => void;
}
diff --git a/src/types/follow.ts b/src/types/follow.ts
index c1183b99..3fd5f735 100644
--- a/src/types/follow.ts
+++ b/src/types/follow.ts
@@ -6,4 +6,5 @@ export interface FollowData {
aliasColor?: string;
followerCount?: number;
isFollowing?: boolean;
+ isMyself?: boolean;
}
diff --git a/src/types/memory.ts b/src/types/memory.ts
index 6b56bae2..d2797bc3 100644
--- a/src/types/memory.ts
+++ b/src/types/memory.ts
@@ -45,6 +45,8 @@ export interface MemoryPostsData {
isbn: string;
nextCursor: string | null;
isLast: boolean;
+ totalPages?: number; // 전체 페이지 수
+ currentUserPage?: number; // 현재 사용자가 읽은 페이지
}
// API 응답 타입
@@ -70,6 +72,7 @@ export interface Record {
pageRange?: string;
isWriter: boolean;
isLiked: boolean; // 좋아요 상태 추가
+ isLocked: boolean; // 블러 처리 여부 추가
pollOptions?: PollOption[];
}
diff --git a/src/types/post.ts b/src/types/post.ts
index f275a192..a329791e 100644
--- a/src/types/post.ts
+++ b/src/types/post.ts
@@ -31,6 +31,7 @@ export interface FeedPostProps extends PostData {
isMyFeed?: boolean;
isTotalFeed?: boolean;
isLast?: boolean;
+ onSaveToggle?: (feedId: number, newSaveState: boolean) => void;
}
export type PostBodyProps = Pick<
diff --git a/src/types/user.ts b/src/types/user.ts
index 5a05c8b6..dd08b47f 100644
--- a/src/types/user.ts
+++ b/src/types/user.ts
@@ -10,4 +10,5 @@ export interface UserProfileItemProps {
userId: number;
isLast?: boolean;
type?: UserProfileType;
+ isMyself?: boolean;
}