feat: 기록을 피드에 핀하기 API 연동#160
Conversation
- 기록 핀하기 API 연동 함수 추가 (pinRecordToFeed.ts) - 기록 아이템에 핀 아이콘 버튼 추가 (내 기록일 때만 표시) - 핀하기 확인 팝업 구현 - 피드 작성 페이지 라우팅 추가 (/feed/write) - 핀 데이터로 피드 작성 페이지 초기화 - 책 선택, 글 내용 수정 불가 옵션 추가
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough레코드 핀 고정 기능을 추가. RecordItem에서 확인 모달 후 pinRecordToFeed API 호출로 데이터를 받아 /feed/write로 이동하며 글쓰기 화면을 사전 채움. CreatePost와 PostContentSection(및 BookSelectionSection)은 읽기 전용 모드 지원. MoreMenu에 핀 버튼 추가. record API 배럴 파일과 신규 API 모듈 도입. 라우트 추가. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant RecordItem
participant MoreMenu
participant ConfirmModal
participant API as pinRecordToFeed(API)
participant Backend
participant Router
participant CreatePost
User->>RecordItem: 레코드 더보기/핀 버튼 클릭
RecordItem->>MoreMenu: openMoreMenu({ onPin })
User->>MoreMenu: "Pin to feed" 클릭
MoreMenu->>RecordItem: onPin 콜백
RecordItem->>ConfirmModal: 확인 모달 표시
User->>ConfirmModal: 확인
ConfirmModal->>API: pinRecordToFeed(roomId, recordId)
API->>Backend: GET /rooms/{roomId}/records/{recordId}/pin
Backend-->>API: PinRecordData
API-->>RecordItem: PinRecordData
RecordItem->>Router: navigate('/feed/write', { state: { pinData, ... } })
Router->>CreatePost: 렌더링(초기 상태 pinData)
CreatePost->>CreatePost: 읽기 전용 모드 설정 및 필드 사전 채움
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
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. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 0
🔭 Outside diff range comments (1)
src/components/memory/RecordItem/RecordItem.tsx (1)
370-373: 댓글 버튼 클릭 시 상위 onClick이 실행되는 버그 가능성 — 버블링을 막아주세요내 기록(isMyRecord=true)에서 댓글 버튼을 누르면 MoreMenu가 뜰 수 있습니다. 좋아요/핀과 동일하게 버블링을 막아주세요.
- <ActionButton> + <ActionButton + onClick={(e) => { + e.stopPropagation(); + // TODO: 댓글 화면 이동 또는 입력 포커스 등 의도된 동작 연결 + }} + aria-label="댓글 보기" + title="댓글 보기" + > <img src={commentIcon} alt="댓글" /> <span>{commentCount}</span> </ActionButton>
🧹 Nitpick comments (17)
src/components/createpost/PostContentSection.tsx (1)
20-27: 텍스트 영역의 readOnly 접근성/UX 보완 제안 (aria 속성, onChange 가드)사용자는 편집 불가 상태를 보조공학으로도 인지할 수 있어야 합니다. 또한 불필요한 onChange 핸들러 실행을 가드하면 의도가 더 명확해집니다.
아래처럼 보완해 주세요.
- onChange={e => onContentChange(e.target.value)} + onChange={e => { if (!readOnly) onContentChange(e.target.value); }} maxLength={maxLength} rows={4} readOnly={readOnly} + aria-readonly={readOnly} + aria-disabled={readOnly} style={{ backgroundColor: readOnly ? '#f5f5f5' : 'transparent', cursor: readOnly ? 'not-allowed' : 'text' }}추가로, 인라인 스타일 대신 Styled Component에
readOnlyprop을 내려 조건부 스타일링을 적용하면 유지보수가 더 수월합니다.src/pages/post/CreatePost.tsx (3)
61-69: pinData 런타임 검증(Type Guard)로 초기값 설정 안정화location.state는 타입 미보장이고, pinData 필드 누락 시 selectedBook 구성이 깨질 수 있습니다. 가벼운 타입 가드로 안전하게 분기하시길 권장합니다.
아래 타입/가드를 파일 상단(인터페이스 근처)에 추가하고:
type PinData = { bookTitle: string; authorName: string; bookImageUrl: string; isbn: string; recordContent?: string; }; const isValidPinData = (v: any): v is PinData => !!v && typeof v.bookTitle === 'string' && typeof v.authorName === 'string' && typeof v.bookImageUrl === 'string' && typeof v.isbn === 'string';해당 초기화 구간을 다음처럼 바꾸면 안전합니다.
- const [selectedBook, setSelectedBook] = useState<Book | null>( - pinData ? { - title: pinData.bookTitle, - author: pinData.authorName, - cover: pinData.bookImageUrl, - isbn: pinData.isbn, - } : convertBookInfoToBook(location.state?.selectedBook), - ); + const [selectedBook, setSelectedBook] = useState<Book | null>( + isValidPinData(pinData) + ? { + title: pinData.bookTitle, + author: pinData.authorName, + cover: pinData.bookImageUrl, + isbn: pinData.isbn, + } + : convertBookInfoToBook(location.state?.selectedBook) + );
48-50: 새로고침 시 location.state 소실 대비 필요 (세션 스토리지/쿼리파라미터 대안)pin 흐름은 state 의존이라 페이지 리프레시 시 데이터가 사라질 수 있습니다. sessionStorage 또는 쿼리파라미터로 최소한의 복원 경로를 두는 것을 권장합니다.
예시(세션 스토리지 fallback): 아래처럼 읽기 + 쓰기를 추가하면 기본 내비게이션/리프레시에 모두 대응됩니다.
- const pinData = location.state?.pinData; + const pinData = + location.state?.pinData ?? + (typeof window !== 'undefined' + ? JSON.parse(sessionStorage.getItem('pinData') || 'null') + : undefined);그리고 CreatePost 마운트 시점에 다음을 추가해 두세요(파일 상단에서 useEffect import 필요).
// pinData가 존재하면 세션에 저장(리프레시 대비) useEffect(() => { if (pinData) { sessionStorage.setItem('pinData', JSON.stringify(pinData)); } }, [pinData]);원하시면 해당 패턴으로 RecordItem 측 네비게이션 코드까지 포함해 전체 패치를 드릴게요.
51-59: convertBookInfoToBook 시그니처와 사용 패턴 정합성내부에서 null 체크를 하고 있으므로 파라미터 타입을
Book | null | undefined로 선언해 두면 의도가 더 명확합니다.예시:
-function convertBookInfoToBook(bookInfo: Book) { +function convertBookInfoToBook(bookInfo: Book | null | undefined) { if (!bookInfo) return null; return { title: bookInfo.title, author: bookInfo.author, cover: bookInfo.cover, isbn: bookInfo.isbn, }; }src/api/record/pinRecordToFeed.ts (3)
15-21: 반환 타입을 명시해 호출부 의도를 더 분명히 해주세요현재 함수는 ApiResponse 래퍼를 그대로 반환합니다. 반환 타입을 명시하면 오해(예: PinRecordData만 반환한다고 오인)를 줄일 수 있습니다.
적용 diff:
-export const pinRecordToFeed = async (roomId: number, recordId: number) => { +export const pinRecordToFeed = async ( + roomId: number, + recordId: number, +): Promise<PinRecordResponse> => { const response = await apiClient.get<PinRecordResponse>( `/rooms/${roomId}/records/${recordId}/pin` ); return response.data; };
23-41: 런타임 예시 주석은 JSDoc 혹은 문서로 이전하는 것을 권장파일 하단의 사용 예시는 좋지만, 소스 내 장문의 사용 예시는 유지보수 시 동기화 이슈를 야기하기 쉽습니다. 간결한 JSDoc로 함수 위에 예시를 남기거나 문서로 이전하는 것을 권장합니다.
불필요한 블록 주석 제거 예:
-/* -사용 예시: -try { - const result = await pinRecordToFeed(1, 123); - if (result.isSuccess) { - console.log("책 제목:", result.data.bookTitle); - console.log("저자명:", result.data.authorName); - console.log("책 이미지:", result.data.bookImageUrl); - console.log("ISBN:", result.data.isbn); - // 성공 처리 로직 - 피드 작성 화면으로 이동 - } else { - console.error("핀하기 실패:", result.message); - // 실패 처리 로직 - } -} catch (error) { - console.error("API 호출 오류:", error); - // 에러 처리 로직 -} -*/
15-16: 함수 네이밍이 동작(핀 수행)처럼 읽힙니다 — 조회 성격을 드러내면 더 명확현재 엔드포인트는 “핀을 수행”하기보다 “핀을 위한 데이터 조회” 성격입니다. fetchPinRecordData/getPinRecordPinInfo 등으로 리네이밍하면 의도가 더 명확합니다. (바렐/호출부 전반 반영 필요)
src/components/common/Modal/MoreMenu.tsx (2)
105-105: 고정 높이(192px) 대신 내용 기반 높이로 전환 권장onPin 미제공 등으로 버튼이 2개일 때도 192px의 빈 공간이 생깁니다. fit-content(또는 auto)로 전환해 레이아웃 일관성을 높이는 것을 권장합니다.
- height: 192px; + height: fit-content;
112-167: 인터랙티브 요소는 button 요소를 사용해 접근성을 개선하세요현재 클릭 가능한 UI를 div로 구현했습니다. button을 쓰면 키보드 포커스/엔터 작동/스크린리더 호환성이 좋아집니다. 스타일 리셋을 함께 적용하여 UI는 동일하게 유지할 수 있습니다.
-const Button = styled.div<{ variant: 'edit' | 'delete' | 'report' | 'pin' }>` +const Button = styled.button<{ variant: 'edit' | 'delete' | 'report' | 'pin' }>` display: flex; height: 50px; align-items: center; + width: 100%; + background: none; + border: none; + text-align: left; + padding: 0; color: ${({ variant }) => { if (variant === 'edit') return colors.white; if (variant === 'delete') return colors.red; if (variant === 'report') return colors.red; if (variant === 'pin') return colors.white; return colors.white; }};추가로, variant === 'pin' 블록에서 위치별 패딩을 별도로 정의하고 있는데, last-of-type 공통 규칙을 활용하면 중복을 더 줄일 수 있습니다.
src/components/memory/RecordItem/RecordItem.tsx (8)
374-383: 핀 버튼에도 접근성 속성(aria-label/title) 추가 권장아이콘만 있는 버튼은 스크린리더에 의도를 명확히 전달하기 위해 라벨을 권장합니다. 이미 버블링 방지는 잘 되어 있습니다.
- {isMyRecord && ( - <ActionButton + {isMyRecord && ( + <ActionButton + aria-label="피드에 핀하기" + title="피드에 핀하기" onClick={(e) => { e.stopPropagation(); // 이벤트 버블링 방지 handlePinConfirm(); }} > <img src={pinIcon} alt="피드에 핀하기" /> </ActionButton> )}
58-58: 타이머 ref 타입은 환경 독립적으로 지정하세요브라우저/Node 타입 혼용 이슈를 피하려면 ReturnType 사용이 안전합니다.
- const longPressTimer = useRef<NodeJS.Timeout | null>(null); + const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
68-69: parseInt 사용 시 기수(radix) 명시 권장암묵적 10진법에 의존하지 않도록 radix를 명시하세요.
- const postId = parseInt(id); + const postId = parseInt(id, 10);
139-139: 기수(radix) 명시 누락같은 맥락에서 여기서도 radix를 명시하세요.
- const recordId = parseInt(record.id); + const recordId = parseInt(record.id, 10);
145-148: currentRoomId 파싱 시 radix 명시- response = await deleteVote(parseInt(currentRoomId), recordId); + response = await deleteVote(parseInt(currentRoomId, 10), recordId); } else { - response = await deleteRecord(parseInt(currentRoomId), recordId); + response = await deleteRecord(parseInt(currentRoomId, 10), recordId); }
202-202: currentRoomId 파싱 시 radix 명시- const response = await pinRecordToFeed(parseInt(currentRoomId), recordId); + const response = await pinRecordToFeed(parseInt(currentRoomId, 10), recordId);
127-136: roomId가 없을 때 '1'로 폴백하는 전략 재검토 권장폴백 값이 실제 존재하는 방으로 연결될 경우, 의도치 않은 방에서 편집/핀 동작이 수행될 수 있습니다. 해당 라우트가 항상 roomId를 보장하는지 확인하거나, 누락 시 에러/가드로 막는 것을 추천합니다.
158-161: 강제 새로고침(window.location.reload) 대신 목록 갱신 콜백 도입 권장UX와 성능 관점에서 전체 리로드는 비용이 큽니다. 상위 컴포넌트에서 삭제된 기록을 제거하는 콜백을 내려받아 상태만 갱신하는 구조를 추천합니다. 필요 시 리팩터링 초안을 제공할 수 있습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
src/assets/feed/pin.svgis excluded by!**/*.svg
📒 Files selected for processing (8)
src/api/record/index.ts(1 hunks)src/api/record/pinRecordToFeed.ts(1 hunks)src/components/common/Modal/MoreMenu.tsx(5 hunks)src/components/createpost/PostContentSection.tsx(2 hunks)src/components/memory/RecordItem/RecordItem.tsx(6 hunks)src/pages/index.tsx(1 hunks)src/pages/post/CreatePost.tsx(3 hunks)src/stores/usePopupStore.ts(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/api/record/pinRecordToFeed.ts (1)
src/api/index.ts (1)
apiClient(7-14)
src/components/common/Modal/MoreMenu.tsx (2)
src/stores/usePopupStore.ts (1)
MoreMenuProps(14-22)src/styles/global/global.ts (1)
colors(4-53)
src/components/memory/RecordItem/RecordItem.tsx (2)
src/api/record/pinRecordToFeed.ts (1)
pinRecordToFeed(16-21)src/components/memory/RecordItem/RecordItem.styled.ts (1)
ActionButton(60-79)
🔇 Additional comments (11)
src/components/createpost/PostContentSection.tsx (1)
7-10: readOnly prop 추가와 기본값 처리, 방향 좋습니다불변 기본값(false)과 함께 읽기 전용 모드 도입이 자연스럽습니다. 이후 상위에서 조건부로 전달하기에도 깔끔합니다.
src/pages/index.tsx (1)
54-54:feed/write경로 사용처 확인 및 충돌 위험 없음다음 위치에서 사용처가 확인되었습니다:
- src/pages/index.tsx (Route 정의, 54행)
- src/components/memory/RecordItem/RecordItem.tsx (navigate('/feed/write', …), 206행)
React Router v6의 경로 랭킹에 따라
feed/write(정적)가feed/:feedId(동적)보다 우선 매칭되므로 충돌 우려는 없습니다. 추가 검증 없이 머지 가능합니다.src/stores/usePopupStore.ts (1)
19-21: MoreMenuProps에 onPin 추가 적절기존 핸들러(onEdit/onDelete/onReport)와 일관된 네이밍으로 확장되어 사용성이 좋습니다. UI가 prop 존재 여부로 조건부 렌더링하기에도 적합합니다.
src/api/record/index.ts (1)
1-6: 배럴 파일 추가로 레코드 API 진입점 정리, 좋습니다관련 API를 단일 엔트리에서 재노출하여 import 경로를 단순화하고, 추후 내부 구조 변경에도 외부 영향이 줄어듭니다.
src/pages/post/CreatePost.tsx (1)
189-193: 읽기 전용 플래그 전달 흐름 일관성 좋습니다핀 유입 시 BookSelectionSection/PostContentSection 모두 readOnly로 동기화하여 UX 일관성이 확보되었습니다.
src/api/record/pinRecordToFeed.ts (2)
12-14: ApiResponse 제네릭 타입 지정 잘 했습니다응답 래퍼 타입을 별칭으로 분리해두어 재사용성과 가독성이 좋습니다.
5-10: 필드 네이밍/스키마가 소비 측(예: CreatePost)과 1:1로 일치하는지 확인 필요
- bookImageUrl, authorName 등의 키가 CreatePost와 BookSelectionSection에서 기대하는 키와 정확히 일치하는지 한번 더 점검해주세요. 내부적으로 thumbnailUrl 등 다른 명칭을 쓰는 곳이 있다면 프론트-백 사이 매핑에서 혼선이 생길 수 있습니다.
src/components/common/Modal/MoreMenu.tsx (2)
13-26: isWriter + onPin 조건부 렌더링 구조가 명확하고 적절합니다내 기록일 때만 핀 버튼을 노출하고, 컨테이너 클릭 버블링을 막는 처리도 좋습니다.
20-24: 핀 버튼 조건부 렌더링 위치 적절기존 편집/삭제와 동일한 계층에 두어 사용성 면에서 자연스럽습니다.
src/components/memory/RecordItem/RecordItem.tsx (2)
360-363: 좋아요 버튼에서 버블링 방지 처리 적절합니다상위 컨테이너 클릭 핸들러와의 충돌을 잘 피하고 있습니다.
206-217: 핀 데이터 state 구성과 네비게이션 흐름 적절합니다CreatePost로 필요한 최소 정보를 안전하게 전달하고 있어 후속 화면에서 읽기 전용 처리하기 용이합니다.
#️⃣ 연관된 이슈
#106
📝 작업 내용
사용자가 기록장에서 자신의 기록을 피드에 핀할 수 있는 기능을 구현했습니다. 이 기능을 통해 기록의 내용을 바탕으로 피드 글을 쉽게 작성할 수 있습니다.
🕸️ 주요 구현 내용
1. API 연동 구현:
/rooms/{roomId}/records/{recordId}/pinGET 엔드포인트를 연동하여 기록 핀하기 기능을 구현했습니다. API 호출 시 책 정보(제목, 저자, 이미지, ISBN)를 받아와서 피드 작성에 활용할 수 있도록 했습니다.2. UI/UX 개선: 기록장의 각 기록 아이템에서 내가 작성한 기록(
isWriter: true)에만 핀 아이콘을 표시하도록 했습니다. 핀 아이콘은 좋아요, 댓글 버튼 옆에 위치하며, 클릭 시 바로 확인 팝업이 표시됩니다.3. 사용자 경험 최적화: 핀 아이콘을 클릭하면 "기록을 피드에 핀하시겠어요?" 확인 팝업이 나타나며, "예"를 선택하면 자동으로 피드 작성 페이지로 이동합니다. 이때 기록의 내용과 책 정보가 자동으로 채워집니다.
4. 라우팅 및 데이터 전달:
/feed/write경로를 추가하여 기존 피드 작성 컴포넌트(CreatePost)를 재사용했습니다. React Router의 state를 통해 핀 데이터(책 정보, 기록 내용 등)를 안전하게 전달했습니다.5. 피드 작성 페이지 개선: 핀 기능을 통해 접근한 경우 책 선택과 글 내용 입력을 수정 불가 상태로 만들어 사용자가 원본 기록의 맥락을 유지하면서도 사진, 공개/비공개 설정, 태그는 자유롭게 설정할 수 있도록 했습니다.
6. 이벤트 처리 개선: 핀 버튼 클릭 시 이벤트 버블링을 방지하여 의도하지 않은 다른 액션(더보기 메뉴 등)이 실행되지 않도록 처리했습니다.
Summary by CodeRabbit