Skip to content

[feature] PhotoUploadCard, MobilePhotoGrid 컴포넌트 구현#1837

Open
suhyun113 wants to merge 9 commits into
develop-fefrom
feature/#1834-photo-edit-mobile-components-MOA-1043
Open

[feature] PhotoUploadCard, MobilePhotoGrid 컴포넌트 구현#1837
suhyun113 wants to merge 9 commits into
develop-fefrom
feature/#1834-photo-edit-mobile-components-MOA-1043

Conversation

@suhyun113

Copy link
Copy Markdown
Collaborator

#️⃣연관된 이슈

#1834

📝작업 내용

모바일 컴포넌트 구현

  • PhotoUploadCard — 이미지 업로드 버튼 모바일 컴포넌트
  • MobilePhotoGrid — 업로드된 사진 그리드 모바일 컴포넌트
  • Storybook 스토리 추가 및 dark_clear_button_icon.svg 아이콘 추가

데스크탑/모바일 공통화

  • 기존 데스크탑 전용이었던 FeedImageGrid와 모바일용 MobilePhotoGridFeedImageGrid로 통합
  • 데스크탑 전용 컴포넌트는 components/desktop/ 폴더로 이동
  • columns prop으로 데스크탑(4열)/모바일(3열) 분기

Figma 스타일 반영

  • gap 2px, border-radius 제거, 삭제 버튼 위치 조정
  • PhotoUploadCard 패딩·텍스트 정렬·아이콘 교체

이미지 로드 실패 처리

  • 서버에서 이미지를 불러오지 못할 경우 브라우저 기본 broken image 아이콘 대신 #D9D9D9 회색 배경 표시
  • broken image 테두리 제거 (border: none)

📖 Storybook: https://67904e61c16daa99a63b44a7-pssyovsivq.chromatic.com/

중점적으로 리뷰받고 싶은 부분(선택)

  • FeedImageGrid에서 columns prop으로 데스크탑/모바일을 분기하는 방식이 적절한지

🫡 참고사항

  • Storybook에서 Admin/PhotoEditTab 스토리로 확인 가능

@vercel

vercel Bot commented Jul 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
moadong Ready Ready Preview, Comment Jul 19, 2026 3:41pm

@github-actions github-actions Bot added ✨ Feature 기능 개발 💻 FE Frontend labels Jul 19, 2026
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@suhyun113, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 21 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2c6d6c0b-ed55-4d99-af39-72c7dded6784

📥 Commits

Reviewing files that changed from the base of the PR and between aec46f6 and d5566ba.

⛔ Files ignored due to path filters (2)
  • frontend/src/assets/images/icons/add_photo_icon.svg is excluded by !**/*.svg
  • frontend/src/assets/images/icons/dark_clear_button_icon.svg is excluded by !**/*.svg
📒 Files selected for processing (10)
  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.stories.tsx
  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/FeedImageGrid/FeedImageGrid.stories.tsx
  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/FeedImageGrid/FeedImageGrid.styles.ts
  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/FeedImageGrid/FeedImageGrid.tsx
  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.stories.tsx
  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.styles.ts
  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.tsx
  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/mobile/PhotoUploadCard/PhotoUploadCard.stories.tsx
  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/mobile/PhotoUploadCard/PhotoUploadCard.styles.ts
  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/mobile/PhotoUploadCard/PhotoUploadCard.tsx
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/#1834-photo-edit-mobile-components-MOA-1043

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@reviewer-harry

reviewer-harry Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @suhyun113's task in 3m 6s —— View job


🧐 harry 리뷰 (총평)

전반적으로 데스크탑/모바일 그리드를 FeedImageGrid로 통합하고 ImagePreview를 정리한 방향이 깔끔해요. Storybook 스토리와 상태별(pending/uploading/failed) 처리도 잘 챙겨주셨어요 👍 몇 가지만 확인 부탁드려요.

  • 리뷰 기준 파일 읽기
  • PR 전체 변경사항 분석
  • 인라인 코멘트 작성
  • 총평 작성

주요 지적 (인라인 코멘트로도 남겼어요)

  1. columns 기본값과 데스크탑 소비처 미갱신FeedImageGridcolumns 기본값이 3인데, 실제 데스크탑 소비처인 PhotoEditTab.tsx:119에서는 columns를 넘기지 않아 데스크탑도 3열로 렌더돼요. 기존 ImageGridrepeat(4, 1fr)이었던 걸 감안하면 "데스크탑 4열" 의도와 어긋나는 회귀로 보여서, 소비처에서 columns={4}를 넘기거나 기본값 방향을 확인해 주세요.
  2. onErrordisplay:none 직접 조작 (minor) — 실패 시 DOM 스타일을 직접 만지면 <img> 재사용 시 인라인 스타일이 남을 수 있어요. 실패 여부를 state로 관리해 조건부 렌더하는 편이 안전해요.
  3. export 컨벤션 (minor)PhotoUploadCardexport default라 폴더 내 named export 컨벤션과 갈려요.

참고로 리뷰받고 싶다고 하신 "columns prop 분기 방식" 자체는 나쁘지 않아요. 다만 소비처에서 실제로 값을 넘겨줘야 의도대로 동작하니 1번을 꼭 확인해 주세요.
· feature/#1834-photo-edit-mobile-components-MOA-1043

@github-actions

Copy link
Copy Markdown

✅ UI 변경사항 없음

구분 링크
📖 Storybook https://67904e61c16daa99a63b44a7-ewgmddkbto.chromatic.com/

전체 86개 스토리 · 32개 컴포넌트

@suhyun113 suhyun113 changed the title feat(admin): 활동사진 편집 모바일 컴포넌트 구현 및 데스크탑/모바일 공통화 [feature] PhotoUploadCard, MobilePhotoGrid 컴포넌트 구현 Jul 19, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 77 to +131
}, [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>
);
})}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

🛠️ 이미지 에러 처리 및 드래그 이벤트 전파 개선

  1. 상태 기반 에러 처리 (React 안티패턴 수정)

    • onError에서 e.currentTarget.style.display = 'none'과 같이 DOM을 직접 조작하면, 드래그 앤 드롭으로 순서가 변경되거나 이미지가 재전송(retry)되어 src가 변경될 때 React가 직접 조작된 인라인 스타일을 인지하지 못합니다. 이로 인해 새로운 이미지가 화면에 표시되지 않고 계속 숨겨지는 버그가 발생할 수 있습니다.
    • useState를 활용해 에러가 발생한 이미지의 URL을 추적하고, 조건부 렌더링을 통해 안전하게 처리하도록 변경합니다.
  2. 드래그 이벤트 전파 방지 (stopPropagation)

    • DeleteButtonRetryButton을 클릭할 때 onMouseDown 이벤트가 부모인 DragItem으로 전파되어 원치 않는 드래그 동작이 시작될 수 있습니다. 이를 방지하기 위해 대화형 요소에 onMouseDown={(e) => e.stopPropagation()}을 추가합니다.
  3. 버튼 타입 명시

    • 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>
        );
      })}

Comment on lines +22 to +25
<Styled.Card onClick={onClick} disabled={disabled}>
<AddPhotoIcon width={56} height={56} />
<Styled.Label>{label}</Styled.Label>
</Styled.Card>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

폼 제출 방지를 위한 버튼 타입 명시

HTML <button> 요소는 기본 typesubmit이므로, 이 컴포넌트가 <form> 내부에서 사용될 경우 의도치 않은 폼 제출 동작을 유발할 수 있습니다. PhotoUploadCard 컴포넌트의 Styled.Card 버튼에 명시적으로 type="button" 속성을 추가하는 것을 권장합니다.

Suggested change
<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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

columns 기본값이 3인데, 실제 데스크탑 소비처인 PhotoEditTab.tsx(119번째 줄 <FeedImageGrid ... />)에서는 columns를 넘기지 않아서 데스크탑도 3열로 렌더돼요. PR 설명의 "데스크탑 4열 / 모바일 3열" 의도와 어긋나고, 기존 ImageGridrepeat(4, 1fr)이었던 점을 감안하면 데스크탑 레이아웃 회귀로 보여요.

PhotoEditTab.tsx에서 columns={4}를 명시적으로 넘겨주거나, 기본값을 어느 쪽에 맞출지 한 번 확인해 주세요. (이 PR 변경 파일에는 PhotoEditTab.tsx가 포함돼 있지 않아서 소비처가 갱신되지 않은 점도 짚어둬요.)

Comment on lines +98 to +100
onError={(e) => {
e.currentTarget.style.display = 'none';
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onError에서 e.currentTarget.style.display = 'none'로 DOM 스타일을 직접 만지면, 이 <img>가 재사용될 때(keysrc는 그대로인데 이후 정상 로드 가능한 상태로 바뀌는 경우) 인라인 display:none이 남아 정상 이미지도 안 보일 수 있어요. 실패 여부를 useState로 관리해 조건부 렌더하는 편이 React 흐름에도 맞고 더 안전해요. (엣지 케이스라 우선순위는 낮아요.)

);
};

export default PhotoUploadCard;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인접한 FeedImageGrid(그리고 이번에 삭제된 ImagePreview)는 named export를 쓰는데 여기만 export default라 폴더 내 export 컨벤션이 갈려요. export const PhotoUploadCard 형태로 맞춰주면 일관성이 좋아요.

@suhyun113
suhyun113 requested a review from seongwon030 July 20, 2026 10:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FE Frontend ✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant