Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions src/pages/mypage/components/PostcardContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
'use client';

import Image from 'next/image';
import { cn } from '@/shared/lib';
import { cva, type VariantProps } from 'class-variance-authority';

interface PostcardContainerProps {
postcards: string[];
onClickCard?: (index: number) => void;
className?: string;
bordered?: boolean;
}

const postcardContainerStyle = cva(
`
layout-grid w-full
rounded-[20px] border bg-pink-50 border-pink-200
p-[1.2rem] gap-[0.6rem]
overflow-y-hidden no-scrollbar
transition-all
`,
{
variants: {
bordered: {
false: 'border-none',
},
},
defaultVariants: {
bordered: true,
},
},
);

const postcardGridStyle = `
grid grid-rows-2 grid-flow-col
auto-cols-[minmax(70px,70px)]
gap-[0.6rem]
`;

const postcardCardStyle = cva(
`
flex aspect-square rounded-[8px]
border border-pink-200 overflow-hidden
bg-gray-100 transition
`,
{
variants: {
interactive: {
true: 'cursor-pointer hover:opacity-80',
false: 'cursor-default',
},
},
defaultVariants: {
interactive: true,
},
},
);

export default function PostcardContainer({
postcards = [],
onClickCard,
className,
bordered,
}: PostcardContainerProps) {
// 엽서 기본 8개 (null placeholder유지) + 9개 부터는 가로 스크롤
const totalSlots = Math.max(postcards.length, 8);
const filledSlots = Array(totalSlots)
.fill(null)
.map((_, i) => postcards[i] ?? null);

return (
<div
className={cn(
postcardContainerStyle({ bordered }),
className,
postcards.length > 8 && 'overflow-x-auto',
)}
>
<div
className={cn(
postcardGridStyle,
postcards.length <= 8 && 'justify-center',
)}
>
{filledSlots.map((src, idx) => (
<button
key={idx}
onClick={() => src && onClickCard?.(idx)}
disabled={!src}
aria-disabled={!src}
className={cn(
postcardCardStyle({ interactive: !!src }),
!src && 'items-center justify-center bg-gray-200',
)}
>
{src && (
<Image
src={src}
alt={`엽서 ${idx + 1}`}
width={200}
height={200}
className="w-full h-full object-cover"
/>
)}
Comment on lines +96 to +104
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

원격 이미지 도메인 허용 여부 확인

Next/Image 사용 시 next.config.(js|ts)images.domains 또는 remotePatterns에 엽서 이미지 도메인이 등록되어 있어야 합니다. 누락 시 빌드/런타임 에러가 납니다.


🏁 Script executed:

#!/bin/bash
# next.config 파일에서 이미지 설정 확인
fd -a 'next.config.*' | xargs -I{} sh -c "echo '--- {} ---'; cat {}"
rg -nP "images\s*:\s*\{|\bdomains\b|\bremotePatterns\b" -n --glob 'next.config.*'

Length of output: 856


next.config.ts에 원격 이미지 도메인 설정 필요

현재 next.config.tsimages.domains 또는 remotePatterns 설정이 없습니다. PostcardContainer.tsx의 Next/Image 컴포넌트가 원격 이미지를 로드할 때 에러가 발생합니다. 사용 중인 이미지 도메인을 next.config.tsimages 설정에 추가하세요.

const nextConfig: NextConfig = {
  images: {
    domains: ['your-image-domain.com'], // 또는 remotePatterns 사용
  },
  // ... 기타 설정
};
🤖 Prompt for AI Agents
In src/pages/mypage/components/PostcardContainer.tsx around lines 96 to 104 the
Next/Image component may load remote images and currently Next.js config lacks
allowed remote image domains, causing runtime errors; open your project's
next.config.ts and add the image host(s) you use under the images key (either as
domains array or remotePatterns) so Next/Image can fetch them, then restart the
dev server to apply the changes.

</button>
))}
</div>
</div>
);

}
35 changes: 35 additions & 0 deletions src/pages/mypage/components/ProfilePhoto.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use client';

import { Icon } from '@/shared/icons';
import { cn } from '@/shared/lib';

interface ProfilePhotoProps {
src?: string;
className?: string;
}

export default function ProfilePhoto({
src,
className,
}: ProfilePhotoProps) {
const baseStyle = `
flex items-center justify-center
h-[14.6rem] w-[14.6rem]
rounded-[20px]
border border-gray-300
bg-gray-100
overflow-hidden
`;

return (
<div className={cn(baseStyle, className)}>
{src ? (
<img src={src} className='w-full h-full object-cover' />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Next.js Image 컴포넌트를 사용하고 접근성을 개선하세요.

현재 구현의 문제점:

  1. 네이티브 <img> 태그 사용으로 Next.js의 이미지 최적화 혜택(자동 리사이징, 포맷 변환, 지연 로딩 등)을 받지 못합니다.
  2. alt 속성이 누락되어 접근성 기준(WCAG)을 충족하지 못합니다.

다음과 같이 수정하세요:

+'use client';
+
+import Image from 'next/image';
 import { Icon } from '@/shared/icons';
 import { cn } from '@/shared/lib';
 
 // ... interface ...

 export default function ProfilePhoto({
   src,
   className,
 }: ProfilePhotoProps) {
   // ... baseStyle ...

   return (
     <div className={cn(baseStyle, className)}>
       {src ? (
-        <img src={src} className='w-full h-full object-cover' />
+        <Image 
+          src={src} 
+          alt="프로필 사진"
+          width={146}
+          height={146}
+          className='w-full h-full object-cover'
+          priority
+        />
       ) : (
         <div className='p-[6.1rem] flex items-center justify-center'>
           <Icon name='User' size={24} color='gray-500' />
         </div>
       )}
     </div>
   );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<img src={src} className='w-full h-full object-cover' />
'use client';
import Image from 'next/image';
import { Icon } from '@/shared/icons';
import { cn } from '@/shared/lib';
// ... interface ...
export default function ProfilePhoto({
src,
className,
}: ProfilePhotoProps) {
// ... baseStyle ...
return (
<div className={cn(baseStyle, className)}>
{src ? (
<Image
src={src}
alt="프로필 사진"
width={146}
height={146}
className='w-full h-full object-cover'
priority
/>
) : (
<div className='p-[6.1rem] flex items-center justify-center'>
<Icon name='User' size={24} color='gray-500' />
</div>
)}
</div>
);
}
🤖 Prompt for AI Agents
In src/pages/mypage/components/ProfilePhoto.tsx around line 27, replace the
native <img> with Next.js' Image component and add an appropriate alt attribute
to satisfy accessibility: import Image from 'next/image' at the top, swap the
<img src={src} className='w-full h-full object-cover' /> for <Image> using
either width/height or layout='fill' (with parent position:relative) to preserve
sizing and object-fit behavior, pass alt (use a descriptive string or alt="" if
purely decorative), keep any needed className or style for object-cover, and
ensure loading behavior (lazy/priority) is set according to importance.

) : (
<div className='p-[6.1rem] flex items-center justify-center'>
<Icon name='User' size={24} color='gray-500' />
</div>
)}
</div>
);
}
73 changes: 73 additions & 0 deletions src/pages/mypage/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use client';

import React, { useState } from 'react';
import ProfilePhoto from '@/pages/mypage/components/ProfilePhoto';
import PostcardContainer from '@/pages/mypage/components/PostcardContainer';
import { EventCard, BottomNav, PopupSet } from '@/shared/components';

export default function MyPage() {
const [showLogoutPopup, setShowLogoutPopup] = useState(false);
const handleLogout = () => {
setShowLogoutPopup(true);
};

return (
<main className='w-full min-h-screen bg-white flex flex-col items-center'>
<div className='w-full flex flex-col items-center px-[2.3rem] pb-[2rem]'>
{/* 프로필 */}
<section className='w-full flex flex-col items-center mt-[2.4rem] gap-[1.0rem]'>
<ProfilePhoto/>
<p className='text-title-md'>이름</p>
</section>
Comment on lines +18 to +21
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

사용자 프로필 데이터를 API 연동으로 교체하세요.

현재 사용자 이름이 "이름"으로 하드코딩되어 있고, ProfilePhoto에 실제 이미지 경로가 전달되지 않습니다. 사용자 정보를 가져오는 API 연동이 필요합니다.

구현 예시:

const [userData, setUserData] = useState({ name: '', profileImage: '' });
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
  fetchUserProfile()
    .then(setUserData)
    .finally(() => setIsLoading(false));
}, []);

// 렌더링:
<ProfilePhoto src={userData.profileImage} />
<p className='text-title-md'>{userData.name}</p>
🤖 Prompt for AI Agents
In src/pages/mypage/index.tsx around lines 18 to 21, the user name is hardcoded
as "이름" and ProfilePhoto is not receiving an image path; replace this with
API-backed user data by adding useState for userData ({ name: '', profileImage:
''}) and loading state, call your fetchUserProfile (or equivalent) inside
useEffect to populate userData (handle errors and set loading false), then
render <ProfilePhoto src={userData.profileImage} /> and <p
className='text-title-md'>{userData.name}</p>, showing a loader or placeholder
while loading or when image/name are absent.


{/* 저장한 행사 */}
<section className='w-full mt-[1.6rem]'>
<p className='text-label-lg mb-[1rem] pl-[1rem]'>저장한 행사</p>
<div className='max-h-[18rem] overflow-y-auto no-scrollbar space-y-[1rem]'>
<EventCard
name='골반 통신 이상 감지'
address='우리집'
description='내 골반이 멈추지 않아서 일까?'
variant='gray'
size='large'
imageSrc=''
/>
<EventCard
name='이혼 숙려 캠프'
address='우리집'
description='앙 이라는 감정'
variant='gray'
size='large'
imageSrc=''
/>
Comment on lines +27 to +42
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

테스트 데이터를 실제 데이터 패칭으로 교체하세요.

하드코딩된 테스트 데이터("골반 통신 이상 감지", "이혼 숙려 캠프")가 포함되어 있습니다. 실제 배포 전에 API 연동 및 데이터 패칭 로직으로 교체되어야 합니다.

실제 이벤트 데이터 패칭 구현 예시:

const [savedEvents, setSavedEvents] = useState<Event[]>([]);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
  // TODO: API 호출로 저장한 이벤트 목록 가져오기
  fetchSavedEvents()
    .then(setSavedEvents)
    .finally(() => setIsLoading(false));
}, []);

🏁 Script executed:

#!/bin/bash
# Examine the target file and lines
if [ -f "src/pages/mypage/index.tsx" ]; then
  echo "=== File found: src/pages/mypage/index.tsx ==="
  echo "=== Lines 1-50 (including target lines 27-42) ==="
  head -n 50 "src/pages/mypage/index.tsx"
else
  echo "File not found at src/pages/mypage/index.tsx"
  find . -name "index.tsx" -path "*/mypage/*" 2>/dev/null | head -20
fi

Length of output: 1822


🏁 Script executed:

#!/bin/bash
# Check for common API/data fetching patterns
if [ -d "src" ]; then
  echo "=== Searching for API integration patterns ==="
  rg -n "fetch|axios|useEffect|API|api" src/pages/mypage/ 2>/dev/null || echo "No results found"
else
  echo "src directory not found"
fi

Length of output: 63


🏁 Script executed:

#!/bin/bash
# Search for EventCard component definition and usage
echo "=== Searching for EventCard component ==="
rg -n "EventCard" -A 5 -B 2 --type tsx 2>/dev/null | head -50

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Check file structure around the target area
if [ -f "src/pages/mypage/index.tsx" ]; then
  echo "=== Context around lines 20-50 ==="
  sed -n '20,50p' "src/pages/mypage/index.tsx"
fi

Length of output: 1017


저장한 행사 데이터를 API 연동으로 교체하세요.

파일 src/pages/mypage/index.tsx 27-42줄에 하드코딩된 테스트 데이터가 포함되어 있습니다. 현재 상태로는 배포 불가능하며, 실제 저장된 행사 데이터를 서버에서 가져오는 로직으로 반드시 교체되어야 합니다. 추가로 PostcardContainer도 빈 배열을 받고 있으므로 함께 수정이 필요합니다.

Comment on lines +27 to +42
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

이거 뭐에요 ㅋㅎㅋㅎㅋㅎㅋㅋ

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

웃고 가세요 :)

</div>
</section>

{/* 저장한 엽서 */}
<section className='w-full mt-[1.8rem]'>
<p className='text-label-lg mb-[0.6rem] pl-[1rem]'>저장한 엽서</p>
<PostcardContainer postcards={[]} />
</section>
Comment on lines +47 to +50
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

저장한 엽서 데이터를 API 연동으로 교체하세요.

PostcardContainer에 빈 배열이 전달되고 있습니다. 사용자가 저장한 엽서 목록을 API에서 가져오는 로직을 구현해야 합니다.

구현 예시:

const [postcards, setPostcards] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
  fetchSavedPostcards()
    .then(setPostcards)
    .finally(() => setIsLoading(false));
}, []);

// 렌더링:
<PostcardContainer postcards={postcards} />


{/* 로그아웃 */}
<button
type='button'
onClick={handleLogout}
className='mt-[2.1em] text-label-md text-gray-400 cursor-pointer underline underline-offset-[0.25rem]'
>
로그아웃
</button>
</div>
<BottomNav />

{showLogoutPopup && (
<PopupSet
text="로그아웃 하시겠습니까?"
onClose={() => {
setShowLogoutPopup(false);
}}
/>
)}
Comment on lines +63 to +70
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

로그아웃 확인 후 실제 로그아웃 로직을 추가하세요.

현재 로그아웃 팝업이 표시되지만, 사용자가 확인을 클릭해도 실제 로그아웃 처리(토큰 제거, 세션 종료, 리다이렉트 등)가 수행되지 않습니다. PopupSet 컴포넌트에 확인 액션 핸들러를 추가하거나, 별도의 로그아웃 로직을 구현해야 합니다.

구현 예시:

const handleConfirmLogout = async () => {
  try {
    await logout(); // API 호출
    // 토큰 제거, 상태 초기화 등
    router.push('/login');
  } catch (error) {
    console.error('로그아웃 실패:', error);
  }
  setShowLogoutPopup(false);
};

// PopupSet에 onConfirm prop이 있다면:
<PopupSet
  text="로그아웃 하시겠습니까?"
  onConfirm={handleConfirmLogout}
  onClose={() => setShowLogoutPopup(false)}
/>
🤖 Prompt for AI Agents
In src/pages/mypage/index.tsx around lines 63 to 70, the logout confirmation
popup is shown but no actual logout logic runs when the user confirms; add an
onConfirm handler to PopupSet that calls the logout flow (await logout API call
if applicable), clears auth tokens/storage, resets any user state,
navigates/redirects to the login (or home) page via the router, handles errors
(log or show a message), and finally closes the popup by setting
setShowLogoutPopup(false); wire the onConfirm prop into the existing PopupSet
invocation and keep the current onClose to just dismiss the dialog.

</main>
);
Comment on lines +14 to +72
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

로딩 및 에러 상태 처리를 추가하세요.

현재 구현에는 다음이 누락되어 있습니다:

  • 데이터 로딩 중 표시할 스켈레톤 UI
  • API 에러 발생 시 에러 메시지 표시
  • 네트워크 실패 시 재시도 로직

기본적인 로딩 상태 처리 예시:

if (isLoading) {
  return <LoadingSkeleton />;
}

if (error) {
  return <ErrorMessage message={error.message} onRetry={refetch} />;
}

}
7 changes: 7 additions & 0 deletions src/shared/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,10 @@ export { default as Header } from './header/Header';
export { default as ControlBar } from './header/ControlBar';
export { default as Tag } from './tag/Tag';
export { DatePicker } from './datePicker/DatePicker';
export { default as LocationBubbleSet } from './set/LocationBubbleSet';
export { default as PopupSet } from './set/PopupSet';
export { default as LocationCard } from './container/LocationCard';
export { default as EventCard } from './container/EventCard';
export { default as AddressCopy } from './button/AddressCopy';
export { default as CommonButton } from './button/CommonButton';
export { BottomNav } from './tab/BottomNav';
Comment on lines +5 to +11
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

👍

2 changes: 2 additions & 0 deletions src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,5 @@ html, body {
@utility max-w-mobile { max-width: var(--max-w-mobile); }
@utility min-w-100 { min-width: var(--min-w-100); }

.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
Comment on lines +222 to +223
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

저희 스크롤바는 따로 안보이게 하는건가요?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

피그마 디자인 상에 기능적으로 스크롤이 들어가야하는 부분은 있지만, 따로 스크롤바는 보이지 않아
일단은 보이지 않도로고 해놓긴 했습니다!

Loading