Skip to content
Merged
Changes from 17 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
575f2d7
✨ feat: 상단(내 가게) section 태그 생성
Moon-ju-young Jun 18, 2025
2b2c5f4
Merge branch 'develop' into feat/90-Store-page
Moon-ju-young Jun 19, 2025
f3f78a9
✨ feat: 초기 api request 추가
Moon-ju-young Jun 19, 2025
1999fdd
✨ feat: 하단(등록한 공고) section 태그 생성
Moon-ju-young Jun 19, 2025
1ef1ff5
🎨 style: section padding 추가
Moon-ju-young Jun 19, 2025
ccc61aa
✨ feat: section 내부 div 태그 추가
Moon-ju-young Jun 19, 2025
7a8f7c7
✨ feat: 비로그인 상태를 위한 Modal 추가
Moon-ju-young Jun 20, 2025
3734f1b
✨ feat: api 처리 오류 시 Modal 추가
Moon-ju-young Jun 20, 2025
dd43c05
Merge branch 'develop' into feat/90-Store-page
Moon-ju-young Jun 20, 2025
713884d
✨ feat: loadNotices 함수, notices state, noticeOffset ref 추가
Moon-ju-young Jun 20, 2025
b8ec8dd
✨ feat: useCallback 적용
Moon-ju-young Jun 20, 2025
8dacbdd
✨ feat: 무한 스크롤 로직 추가
Moon-ju-young Jun 20, 2025
a115c37
✨ feat: 제목(h1) 추가
Moon-ju-young Jun 20, 2025
38c17b9
✨ feat: isMobile state 추가 및 내 가게 카드 구현
Moon-ju-young Jun 20, 2025
e5c1d5e
✨ feat: 등록 공고 부분 구현
Moon-ju-young Jun 20, 2025
89f615b
Merge branch 'develop' into feat/90-Store-page
Moon-ju-young Jun 20, 2025
3ad6878
✨ feat: Footer 추가 및 스크롤 감지 요소 스타일 수정
Moon-ju-young Jun 20, 2025
d9a75ab
Merge branch 'develop' into feat/90-Store-page
Moon-ju-young Jun 21, 2025
b7a24d2
♻️ refactor: 변경된 Post에 맞게 수정
Moon-ju-young Jun 21, 2025
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
192 changes: 191 additions & 1 deletion src/pages/store/Store.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,193 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { getUser } from '@/api/userApi';
import type { ShopItem } from '@/api/shopApi';
import { getShopNotices, type NoticeInfo } from '@/api/noticeApi';
import Modal from '@/components/common/Modal';
import RegisterLayout from '@/components/layout/RegisterLayout';
import Button from '@/components/common/Button';
import ic_location from '@/assets/icons/location-red.svg';
import Post from '@/components/common/Post';
import Footer from '@/components/layout/Footer';

const NOTICES_LIMIT = 12;

export default function Store() {
return <div>내 가게 정보 상세(사장님)</div>;
const navigate = useNavigate();
const [shop, setShop] = useState<ShopItem | null>(null);
const [notices, setNotices] = useState<NoticeInfo[]>([]);
const noticesOffset = useRef(0);
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalContent, setModalContent] = useState('');
const [isLoading, setIsLoading] = useState<boolean | 'error'>(false);
const observerRef = useRef<HTMLDivElement>(null);
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);

const handleClose = useCallback(() => {
if (modalContent.startsWith('로그인')) {
navigate('/login');
} else {
setIsModalOpen(false);
}
}, [navigate, modalContent]);

const loadNotices = useCallback(async () => {
if (!shop || noticesOffset.current < 0) return;
setIsLoading(true);
try {
const noticesResponse = await getShopNotices(shop.id, {
offset: noticesOffset.current,
limit: NOTICES_LIMIT,
});
setNotices((prev) => [...prev, ...noticesResponse.items]);
if (noticesResponse.hasNext) noticesOffset.current += NOTICES_LIMIT;
else noticesOffset.current = -1;
setIsLoading(false);
} catch {
setIsLoading('error');
}
}, [shop]);

useEffect(() => {
const userId = localStorage.getItem('userId');
if (!userId) {
setIsModalOpen(true);
setModalContent('로그인이 필요합니다.');
return;
}

try {
(async () => {
const userResponse = await getUser(userId);
setShop(userResponse.item.shop?.item ?? null);
})();
} catch (error) {
setIsModalOpen(true);
setModalContent((error as Error).message);
}
}, []);

// 무한 스크롤
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && !isLoading) {
loadNotices();
}
},
{
rootMargin: '300px',
},
);

const { current } = observerRef;
if (current) observer.observe(current);
return () => {
if (current) observer.unobserve(current);
};
}, [loadNotices, isLoading]);

// 반응형
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 768);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);

return (
<div className="flex min-h-[calc(100vh-108px)] flex-col justify-between md:min-h-[calc(100vh_-_70px)]">
<section className="w-full bg-white px-12 py-40 md:px-32 md:py-60">
<div className="mx-auto max-w-964">
<h1 className="mb-16 text-h1/34 font-bold md:mb-24">내 가게</h1>
{shop ? (
<div className="flex min-h-356 flex-col items-stretch justify-between rounded-xl bg-red-10 p-20 md:p-24 lg:flex-row">
<div
className="h-178 rounded-xl bg-cover bg-center md:h-360 lg:h-auto lg:w-539"
style={{ backgroundImage: `url(${shop.imageUrl})` }}
/>
<div className="mt-12 flex flex-col justify-between gap-24 md:mt-16 md:gap-40 lg:w-346">
<div className="flex flex-col gap-8 text-body2/22 font-regular md:gap-12 md:text-body1/26">
<div className="font-bold">
<div className="mb-8 leading-17 text-primary md:leading-20">
식당
</div>
<div className="text-h2/29 md:text-h1/34">{shop.name}</div>
</div>
<div className="flex items-center gap-6">
<img className="size-16 md:size-20" src={ic_location} />
<span className="text-gray-50">{shop.address1}</span>
</div>
<div className="text-[#000]">{shop.description}</div>
</div>
<div className="grid grid-cols-2 gap-8">
<Button
size={isMobile ? 'medium' : 'large'}
solid={false}
onClick={() => navigate('/owner/store/edit')}
>
편집하기
</Button>
<Button
size={isMobile ? 'medium' : 'large'}
solid={true}
onClick={() => navigate('/owner/post')}
>
공고 등록하기
</Button>
</div>
</div>
</div>
) : (
<RegisterLayout type="store" />
)}
</div>
</section>
{shop && (
<section className="w-full flex-1 bg-gray-5 px-12 pt-40 pb-80 md:px-32 md:pt-60 md:pb-120">
<div className="mx-auto max-w-964">
<h1 className="mb-16 text-h1/34 font-bold md:mb-32">
{notices[0] && '내가 '}등록한 공고
</h1>
{notices[0] ? (
<div className="grid grid-cols-2 gap-x-9 gap-y-16 md:gap-x-14 md:gap-y-32 lg:grid-cols-3">
{notices.map((notice) => (
<Link
key={notice.item.id}
to={`/owner/post/${notice.item.id}`}
>
<Post
data={{ ...notice.item, shop: { item: shop, href: '' } }}
/>
</Link>
))}
</div>
) : (
<RegisterLayout type="notice" />
)}
</div>
{/* 스크롤 감지 요소 */}
<div
className="text-center text-caption text-gray-30"
ref={observerRef}
>
{isLoading && (
<div className="mt-32">
{isLoading === 'error'
? '데이터를 불러오는데 실패했습니다.'
: '로딩 중...'}
</div>
)}
</div>
</section>
)}
<Footer />
{isModalOpen && (
<Modal onButtonClick={handleClose} onClose={handleClose}>
{modalContent}
</Modal>
)}
</div>
);
}