-
Notifications
You must be signed in to change notification settings - Fork 1
✨Feat : MyPage 작업 #67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
295fed0
96eb415
813a4df
a2a4cca
8c6b2f9
590c770
f15b7b3
1a0d537
daba470
305da77
5b70510
593e0e5
eb92bf6
7d5c023
e25c7ad
edcb5d5
03a6115
50b2584
8f54dca
57b7c57
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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" | ||
| /> | ||
| )} | ||
| </button> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| ); | ||
|
|
||
| } | ||
| 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' /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Next.js 현재 구현의 문제점:
다음과 같이 수정하세요: +'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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className='p-[6.1rem] flex items-center justify-center'> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Icon name='User' size={24} color='gray-500' /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사용자 프로필 데이터를 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 |
||
|
|
||
| {/* 저장한 행사 */} | ||
| <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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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
fiLength 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"
fiLength 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 -50Length 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"
fiLength of output: 1017 저장한 행사 데이터를 API 연동으로 교체하세요. 파일
Comment on lines
+27
to
+42
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이거 뭐에요 ㅋㅎㅋㅎㅋㅎㅋㅋ
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저장한 엽서 데이터를 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]' | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| > | ||
| 로그아웃 | ||
| </button> | ||
| </div> | ||
| <BottomNav /> | ||
|
|
||
| {showLogoutPopup && ( | ||
| <PopupSet | ||
| text="로그아웃 하시겠습니까?" | ||
| onClose={() => { | ||
| setShowLogoutPopup(false); | ||
| }} | ||
| /> | ||
| )} | ||
|
Comment on lines
+63
to
+70
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 로그아웃 확인 후 실제 로그아웃 로직을 추가하세요. 현재 로그아웃 팝업이 표시되지만, 사용자가 확인을 클릭해도 실제 로그아웃 처리(토큰 제거, 세션 종료, 리다이렉트 등)가 수행되지 않습니다. 구현 예시: 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 |
||
| </main> | ||
| ); | ||
|
Comment on lines
+14
to
+72
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 로딩 및 에러 상태 처리를 추가하세요. 현재 구현에는 다음이 누락되어 있습니다:
기본적인 로딩 상태 처리 예시: if (isLoading) {
return <LoadingSkeleton />;
}
if (error) {
return <ErrorMessage message={error.message} onRetry={refetch} />;
} |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저희 스크롤바는 따로 안보이게 하는건가요?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 피그마 디자인 상에 기능적으로 스크롤이 들어가야하는 부분은 있지만, 따로 스크롤바는 보이지 않아 |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
원격 이미지 도메인 허용 여부 확인
Next/Image 사용 시
next.config.(js|ts)의images.domains또는remotePatterns에 엽서 이미지 도메인이 등록되어 있어야 합니다. 누락 시 빌드/런타임 에러가 납니다.🏁 Script executed:
Length of output: 856
next.config.ts에 원격 이미지 도메인 설정 필요현재
next.config.ts에images.domains또는remotePatterns설정이 없습니다. PostcardContainer.tsx의 Next/Image 컴포넌트가 원격 이미지를 로드할 때 에러가 발생합니다. 사용 중인 이미지 도메인을next.config.ts의images설정에 추가하세요.🤖 Prompt for AI Agents