[feature] PhotoUploadCard, MobilePhotoGrid 컴포넌트 구현#1837
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 21 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (10)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
Claude finished @suhyun113's task in 3m 6s —— View job 🧐 harry 리뷰 (총평)전반적으로 데스크탑/모바일 그리드를
주요 지적 (인라인 코멘트로도 남겼어요)
|
✅ UI 변경사항 없음
전체 86개 스토리 · 32개 컴포넌트 |
There was a problem hiding this comment.
Code Review
This pull request refactors the photo editing administration tab by removing the standalone 'ImagePreview' component and integrating its preview, upload status, and deletion logic directly into 'FeedImageGrid'. It also introduces a new 'PhotoUploadCard' component for mobile photo uploads, adds new SVG icons, and updates the corresponding Storybook stories. The review feedback recommends improving the image error handling in 'FeedImageGrid' by using React state instead of direct DOM manipulation, preventing drag event propagation on interactive buttons, and explicitly setting the button types to 'button' to avoid accidental form submissions.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| }, [dividerIndex]); | ||
|
|
||
| return ( | ||
| <Styled.ImageGrid ref={gridRef}> | ||
| {feedItems.map((item, index) => ( | ||
| <Styled.DragItem | ||
| key={item.type === 'uploaded' ? item.url : item.previewUrl} | ||
| data-card-index={index} | ||
| onMouseDown={(e) => onMouseDown(e, index)} | ||
| $isDragging={dragIndex === index} | ||
| $isDimmed={dragIndex !== null && dragIndex !== index} | ||
| > | ||
| <ImagePreview | ||
| image={item.type === 'uploaded' ? item.url : item.previewUrl} | ||
| status={item.type === 'local' ? item.status : undefined} | ||
| disabled={isLoading} | ||
| onDelete={() => onDelete(index)} | ||
| onRetry={ | ||
| item.type === 'local' && item.status === 'failed' | ||
| ? () => onRetry(index) | ||
| : undefined | ||
| } | ||
| /> | ||
| </Styled.DragItem> | ||
| ))} | ||
| <Styled.Grid ref={gridRef} $columns={columns}> | ||
| {feedItems.map((item, index) => { | ||
| const src = item.type === 'uploaded' ? item.url : item.previewUrl; | ||
| const status = item.type === 'local' ? item.status : undefined; | ||
|
|
||
| return ( | ||
| <Styled.DragItem | ||
| key={src} | ||
| data-card-index={index} | ||
| onMouseDown={(e) => onMouseDown(e, index)} | ||
| $isDragging={dragIndex === index} | ||
| $isDimmed={dragIndex !== null && dragIndex !== index} | ||
| > | ||
| <Styled.PhotoItem> | ||
| <Styled.Photo | ||
| src={src} | ||
| alt='' | ||
| draggable={false} | ||
| onError={(e) => { | ||
| e.currentTarget.style.display = 'none'; | ||
| }} | ||
| /> | ||
|
|
||
| {status === 'uploading' && ( | ||
| <Styled.Overlay> | ||
| <Styled.StatusText>업로드 중</Styled.StatusText> | ||
| </Styled.Overlay> | ||
| )} | ||
| {status === 'failed' && ( | ||
| <Styled.Overlay $error> | ||
| <Styled.StatusText>실패</Styled.StatusText> | ||
| <Styled.RetryButton onClick={() => onRetry(index)}> | ||
| 재전송 | ||
| </Styled.RetryButton> | ||
| </Styled.Overlay> | ||
| )} | ||
| {status === 'pending' && ( | ||
| <Styled.PendingBadge>업로드 예정</Styled.PendingBadge> | ||
| )} | ||
|
|
||
| <Styled.DeleteButton | ||
| type='button' | ||
| onClick={() => onDelete(index)} | ||
| disabled={isLoading || status === 'uploading'} | ||
| aria-label='사진 삭제' | ||
| > | ||
| <ClearButtonIcon /> | ||
| </Styled.DeleteButton> | ||
| </Styled.PhotoItem> | ||
| </Styled.DragItem> | ||
| ); | ||
| })} |
There was a problem hiding this comment.
🛠️ 이미지 에러 처리 및 드래그 이벤트 전파 개선
-
상태 기반 에러 처리 (React 안티패턴 수정)
onError에서e.currentTarget.style.display = 'none'과 같이 DOM을 직접 조작하면, 드래그 앤 드롭으로 순서가 변경되거나 이미지가 재전송(retry)되어src가 변경될 때 React가 직접 조작된 인라인 스타일을 인지하지 못합니다. 이로 인해 새로운 이미지가 화면에 표시되지 않고 계속 숨겨지는 버그가 발생할 수 있습니다.useState를 활용해 에러가 발생한 이미지의 URL을 추적하고, 조건부 렌더링을 통해 안전하게 처리하도록 변경합니다.
-
드래그 이벤트 전파 방지 (
stopPropagation)DeleteButton과RetryButton을 클릭할 때onMouseDown이벤트가 부모인DragItem으로 전파되어 원치 않는 드래그 동작이 시작될 수 있습니다. 이를 방지하기 위해 대화형 요소에onMouseDown={(e) => e.stopPropagation()}을 추가합니다.
-
버튼 타입 명시
RetryButton에 명시적으로type="button"속성을 추가하여 의도치 않은 폼 제출 동작을 방지합니다.
}, [dividerIndex]);
const [failedUrls, setFailedUrls] = useState<Record<string, boolean>>({});
return (
<Styled.Grid ref={gridRef} $columns={columns}>
{feedItems.map((item, index) => {
const src = item.type === 'uploaded' ? item.url : item.previewUrl;
const status = item.type === 'local' ? item.status : undefined;
return (
<Styled.DragItem
key={src}
data-card-index={index}
onMouseDown={(e) => onMouseDown(e, index)}
$isDragging={dragIndex === index}
$isDimmed={dragIndex !== null && dragIndex !== index}
>
<Styled.PhotoItem>
{!failedUrls[src] && (
<Styled.Photo
src={src}
alt=''
draggable={false}
onError={() => {
setFailedUrls((prev) => ({ ...prev, [src]: true }));
}}
/>
)}
{status === 'uploading' && (
<Styled.Overlay>
<Styled.StatusText>업로드 중</Styled.StatusText>
</Styled.Overlay>
)}
{status === 'failed' && (
<Styled.Overlay $error>
<Styled.StatusText>실패</Styled.StatusText>
<Styled.RetryButton
type='button'
onMouseDown={(e) => e.stopPropagation()}
onClick={() => {
setFailedUrls((prev) => {
const next = { ...prev };
delete next[src];
return next;
});
onRetry(index);
}}
>
재전송
</Styled.RetryButton>
</Styled.Overlay>
)}
{status === 'pending' && (
<Styled.PendingBadge>업로드 예정</Styled.PendingBadge>
)}
<Styled.DeleteButton
type='button'
onMouseDown={(e) => e.stopPropagation()}
onClick={() => onDelete(index)}
disabled={isLoading || status === 'uploading'}
aria-label='사진 삭제'
>
<ClearButtonIcon />
</Styled.DeleteButton>
</Styled.PhotoItem>
</Styled.DragItem>
);
})}
| <Styled.Card onClick={onClick} disabled={disabled}> | ||
| <AddPhotoIcon width={56} height={56} /> | ||
| <Styled.Label>{label}</Styled.Label> | ||
| </Styled.Card> |
There was a problem hiding this comment.
폼 제출 방지를 위한 버튼 타입 명시
HTML <button> 요소는 기본 type이 submit이므로, 이 컴포넌트가 <form> 내부에서 사용될 경우 의도치 않은 폼 제출 동작을 유발할 수 있습니다. PhotoUploadCard 컴포넌트의 Styled.Card 버튼에 명시적으로 type="button" 속성을 추가하는 것을 권장합니다.
| <Styled.Card onClick={onClick} disabled={disabled}> | |
| <AddPhotoIcon width={56} height={56} /> | |
| <Styled.Label>{label}</Styled.Label> | |
| </Styled.Card> | |
| <Styled.Card type='button' onClick={onClick} disabled={disabled}> | |
| <AddPhotoIcon width={56} height={56} /> | |
| <Styled.Label>{label}</Styled.Label> | |
| </Styled.Card> |
| dragIndex, | ||
| dropPosition, | ||
| isLoading, | ||
| columns = 3, |
There was a problem hiding this comment.
columns 기본값이 3인데, 실제 데스크탑 소비처인 PhotoEditTab.tsx(119번째 줄 <FeedImageGrid ... />)에서는 columns를 넘기지 않아서 데스크탑도 3열로 렌더돼요. PR 설명의 "데스크탑 4열 / 모바일 3열" 의도와 어긋나고, 기존 ImageGrid가 repeat(4, 1fr)이었던 점을 감안하면 데스크탑 레이아웃 회귀로 보여요.
PhotoEditTab.tsx에서 columns={4}를 명시적으로 넘겨주거나, 기본값을 어느 쪽에 맞출지 한 번 확인해 주세요. (이 PR 변경 파일에는 PhotoEditTab.tsx가 포함돼 있지 않아서 소비처가 갱신되지 않은 점도 짚어둬요.)
| onError={(e) => { | ||
| e.currentTarget.style.display = 'none'; | ||
| }} |
There was a problem hiding this comment.
onError에서 e.currentTarget.style.display = 'none'로 DOM 스타일을 직접 만지면, 이 <img>가 재사용될 때(key인 src는 그대로인데 이후 정상 로드 가능한 상태로 바뀌는 경우) 인라인 display:none이 남아 정상 이미지도 안 보일 수 있어요. 실패 여부를 useState로 관리해 조건부 렌더하는 편이 React 흐름에도 맞고 더 안전해요. (엣지 케이스라 우선순위는 낮아요.)
| ); | ||
| }; | ||
|
|
||
| export default PhotoUploadCard; |
There was a problem hiding this comment.
인접한 FeedImageGrid(그리고 이번에 삭제된 ImagePreview)는 named export를 쓰는데 여기만 export default라 폴더 내 export 컨벤션이 갈려요. export const PhotoUploadCard 형태로 맞춰주면 일관성이 좋아요.
#️⃣연관된 이슈
📝작업 내용
모바일 컴포넌트 구현
PhotoUploadCard— 이미지 업로드 버튼 모바일 컴포넌트MobilePhotoGrid— 업로드된 사진 그리드 모바일 컴포넌트dark_clear_button_icon.svg아이콘 추가데스크탑/모바일 공통화
FeedImageGrid와 모바일용MobilePhotoGrid를FeedImageGrid로 통합components/desktop/폴더로 이동columnsprop으로 데스크탑(4열)/모바일(3열) 분기Figma 스타일 반영
이미지 로드 실패 처리
#D9D9D9회색 배경 표시border: none)📖 Storybook: https://67904e61c16daa99a63b44a7-pssyovsivq.chromatic.com/
중점적으로 리뷰받고 싶은 부분(선택)
FeedImageGrid에서columnsprop으로 데스크탑/모바일을 분기하는 방식이 적절한지🫡 참고사항
Admin/PhotoEditTab스토리로 확인 가능