Conversation
There was a problem hiding this comment.
Pull request overview
랜딩페이지에 “후원(계좌 복사)” 섹션을 추가해 사용자가 후원 계좌를 쉽게 복사하고, 복사 상태에 따라 UI(아이콘/이미지/툴팁)가 변경되도록 하는 PR입니다.
Changes:
- 랜딩페이지에
DonationSection을 추가하고 페이지에 포함 - 계좌 복사 상태를 공유하는 Context 및 복사 버튼 UI 구현
- 공용
Icon컴포넌트(clipboard/check) 추가
Reviewed changes
Copilot reviewed 7 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/shared/components/foundation/solar_copy-line-duotone | (추가) SVG 원본 파일 추가 |
| src/shared/components/foundation/Icons.tsx | clipboard/check 아이콘 및 Icon 컴포넌트 추가 |
| src/composite/landing/DonationSection/index.tsx | 후원 섹션 레이아웃 구성 |
| src/composite/landing/DonationSection/DonationImage.tsx | 복사 상태에 따라 이미지/툴팁 전환 |
| src/composite/landing/DonationSection/CopyAccountContext.tsx | 복사 상태 Context/Provider 추가 |
| src/composite/landing/DonationSection/CopyAccountButton.tsx | 클립보드 복사 버튼 추가 |
| src/app/page.tsx | 랜딩 페이지에 DonationSection 추가 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| type IconComponentProps = React.SVGProps<SVGSVGElement> & { name: string; className?: string }; | ||
|
|
||
| const ClipboardIcon = ({ ...props }: IconComponentProps) => { | ||
| return ( | ||
| <svg | ||
| width={props.width ?? '24'} | ||
| height={props.height ?? '24'} | ||
| viewBox="0 0 24 24" | ||
| fill="none" | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| {...props} | ||
| className={props.className} | ||
| > |
There was a problem hiding this comment.
Icon passes the name prop down into ClipboardIcon/CheckIcon, and those components spread ...props onto the <svg>. This results in a non-standard name attribute being rendered on the SVG (and React will warn about unknown DOM attributes). Consider separating the public Icon props (with name) from the internal SVG props, or destructuring name out before spreading onto <svg>/forwarding to the icon component.
| type IconComponentProps = React.SVGProps<SVGSVGElement> & { name: string; className?: string }; | ||
|
|
||
| const ClipboardIcon = ({ ...props }: IconComponentProps) => { | ||
| return ( | ||
| <svg | ||
| width={props.width ?? '24'} | ||
| height={props.height ?? '24'} | ||
| viewBox="0 0 24 24" | ||
| fill="none" | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| {...props} | ||
| className={props.className} | ||
| > | ||
| <path | ||
| d="M6 11C6 8.172 6 6.757 6.879 5.879C7.757 5 9.172 5 12 5H15C17.828 5 19.243 5 20.121 5.879C21 6.757 21 8.172 21 11V16C21 18.828 21 20.243 20.121 21.121C19.243 22 17.828 22 15 22H12C9.172 22 7.757 22 6.879 21.121C6 20.243 6 18.828 6 16V11Z" | ||
| stroke={props.stroke ?? 'currentColor'} | ||
| strokeWidth={props.strokeWidth ?? '1.5'} | ||
| /> | ||
| <path | ||
| opacity="0.5" | ||
| d="M6 19C5.20435 19 4.44129 18.6839 3.87868 18.1213C3.31607 17.5587 3 16.7956 3 16V10C3 6.229 3 4.343 4.172 3.172C5.344 2.001 7.229 2 11 2H15C15.7956 2 16.5587 2.31607 17.1213 2.87868C17.6839 3.44129 18 4.20435 18 5" | ||
| stroke={props.stroke ?? 'currentColor'} | ||
| strokeWidth={props.strokeWidth ?? '1.5'} | ||
| /> | ||
| </svg> | ||
| ); | ||
| }; | ||
|
|
||
| const CheckIcon = ({ ...props }: IconComponentProps) => { | ||
| return ( | ||
| <svg | ||
| width={props.width ?? '24'} | ||
| height={props.height ?? '24'} | ||
| viewBox="0 0 24 24" | ||
| fill="none" | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| {...props} | ||
| className={props.className} | ||
| > | ||
| <path | ||
| d="M20 6L9 17L4 12" | ||
| stroke={props.stroke ?? 'currentColor'} | ||
| strokeWidth={props.strokeWidth ?? '2'} | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| /> | ||
| </svg> | ||
| ); | ||
| }; | ||
|
|
||
| const iconMap = { | ||
| clipboard: ClipboardIcon, | ||
| check: CheckIcon, | ||
| }; | ||
|
|
||
| export const Icon = ({ className = '', ...props }: IconComponentProps) => { | ||
| const IconComponent = iconMap[props.name as keyof typeof iconMap]; | ||
| if (!IconComponent) { |
There was a problem hiding this comment.
IconComponentProps types name as a plain string, which defeats type-safety and requires casting when indexing iconMap. Prefer typing name as keyof typeof iconMap (or exporting a type IconName = ...) so callers can only pass supported icon names and you can remove the as cast.
| <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| <path d="M6 11C6 8.172 6 6.757 6.879 5.879C7.757 5 9.172 5 12 5H15C17.828 5 19.243 5 20.121 5.879C21 6.757 21 8.172 21 11V16C21 18.828 21 20.243 20.121 21.121C19.243 22 17.828 22 15 22H12C9.172 22 7.757 22 6.879 21.121C6 20.243 6 18.828 6 16V11Z" stroke="black" stroke-width="1.5"/> | ||
| <path opacity="0.5" d="M6 19C5.20435 19 4.44129 18.6839 3.87868 18.1213C3.31607 17.5587 3 16.7956 3 16V10C3 6.229 3 4.343 4.172 3.172C5.344 2.001 7.229 2 11 2H15C15.7956 2 16.5587 2.31607 17.1213 2.87868C17.6839 3.44129 18 4.20435 18 5" stroke="black" stroke-width="1.5"/> | ||
| </svg> |
There was a problem hiding this comment.
This file appears to be an unreferenced raw SVG sitting under components/foundation with no extension and no imports/usages in the codebase. As-is it’s dead code and can be confusing for bundling/linting. Either remove it, rename/move it to a proper .svg asset location (e.g. public/), or convert it into a TSX icon component and import it where needed.
| export const useCopyAccountContext = () => { | ||
| const context = useContext(CopyAccountContext); | ||
| if (!context) { | ||
| throw new Error('useCopyAccountContext must be used within a CopyAccountProvider'); |
There was a problem hiding this comment.
The thrown error message mentions CopyAccountProvider, but the exported provider component is CopyAccountContextProvider. This mismatch can mislead debugging; update the message to match the actual provider name.
| throw new Error('useCopyAccountContext must be used within a CopyAccountProvider'); | |
| throw new Error('useCopyAccountContext must be used within a CopyAccountContextProvider'); |
| const handleCopyToClipboard = useCallback(() => { | ||
| navigator.clipboard.writeText('3333-35-3209232'); | ||
| setIsCopied(true); |
There was a problem hiding this comment.
navigator.clipboard.writeText is async and can reject (permission, insecure context, unsupported browser). Currently the code sets isCopied to true regardless of whether the write succeeded and doesn’t handle failures. Consider awaiting the promise, guarding for navigator.clipboard existence, and only setting isCopied on success (optionally surface an error/toast on failure).
| const handleCopyToClipboard = useCallback(() => { | |
| navigator.clipboard.writeText('3333-35-3209232'); | |
| setIsCopied(true); | |
| const handleCopyToClipboard = useCallback(async () => { | |
| if (!navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') { | |
| console.error('Clipboard API is not available.'); | |
| return; | |
| } | |
| try { | |
| await navigator.clipboard.writeText('3333-35-3209232'); | |
| setIsCopied(true); | |
| } catch (error) { | |
| console.error('Failed to copy account number to clipboard:', error); | |
| } |
|
|
||
| const renderToolTipText = () => { | ||
| if (isCopied) { | ||
| return '보내주신 마음 잊지 않고 \n 더 열심히 할게요!'; |
There was a problem hiding this comment.
renderToolTipText() returns a string containing \n, but ToolTip renders text inside an element with whitespace-nowrap, so the newline won’t display as an actual line break. If you want a multi-line tooltip, pass a ReactNode with <br /> (or update ToolTip styling to whitespace-pre-line).
| return '보내주신 마음 잊지 않고 \n 더 열심히 할게요!'; | |
| return ( | |
| <> | |
| 보내주신 마음 잊지 않고 <br /> | |
| 더 열심히 할게요! | |
| </> | |
| ); |
| height={650} | ||
| sizes="(max-width: 768px) 200px, 350px" | ||
| className="w-50 md:w-87.5 h-auto shrink-0" | ||
| alt="donation-section-image" |
There was a problem hiding this comment.
The image alt text ("donation-section-image") is not very descriptive for screen readers. Consider using an alt that conveys the meaning/purpose of the image (or alt="" if it’s purely decorative and the surrounding text already covers the content).
| alt="donation-section-image" | |
| alt={isCopied ? '후원에 감사하는 일러스트 이미지' : '후원을 요청하는 일러스트 이미지'} |
No description provided.