Skip to content
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

#54 style: 농구장 자세히 보기 UI 수정 #77

Merged
merged 3 commits into from Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Binary file added public/images/han-river-park-court.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/app/(auth)/user-info/page.tsx
Expand Up @@ -31,7 +31,7 @@ const SignUpProcess = () => {
} else {
try {
// await axiosInstance.patch('/api/user/update/info', userInfo);
router.push('/login');
router.push('/');
} catch (error) {
console.error('정보 전송 실패:', error);
}
Expand Down
309 changes: 166 additions & 143 deletions src/app/map/components/CourtDetail.tsx
@@ -1,192 +1,215 @@
'use client';

import React, { useEffect, useState } from 'react';
import { Textarea, Input, RadioGroup, Radio, Button } from '@nextui-org/react';
import React from 'react';
import { Button } from '@nextui-org/react';
import { IoIosClose } from 'react-icons/io';
import { FaPhone } from 'react-icons/fa';
import { PiChatsCircle } from 'react-icons/pi';
import { FaPhoneAlt, FaParking, FaTag, FaRegDotCircle } from 'react-icons/fa';
import Image from 'next/image';
import { FaLocationDot, FaClock, FaLightbulb } from 'react-icons/fa6';
import { PiChatsCircle } from 'react-icons/pi';
import { CourtIcon } from './icons/CourtIcon';
import { HoopIcon } from './icons/HoopIcon';
import { FeeIcon } from './icons/FeeIcon';
import { InfoIcon } from './icons/InfoIcon';
import { WebsiteIcon } from './icons/WebsiteIcon';

interface CourtDetailsProps {
isVisible: boolean;
onClose: () => void;
selectedPlace: any;
}

// 농구장 사진, 주소(도로명), 코트 종류, 실내외(실내/야외), 코트사이즈, 골대수, 야간 조명, 개방시간, 사용료, 주차 가능, 전화번호, 홈페이지, 기타 정보
// [TODO] 주소 복사 넣기, ✅
// 각 컨텐츠 아이콘 넣기 ✅
// true, false 명확히하기 ✅
// 현재 링크 공유 만들기

const CourtDetails: React.FC<CourtDetailsProps> = ({
isVisible,
onClose,
selectedPlace,
}) => {
const [otherInfo, setOtherInfo] = useState<string>(
'주차하기 어려워요. 대중교통 이용 추천해요.'
);
const [isInfoModified, setIsInfoModified] = useState<boolean>(false);

// api 개발 완료 후 주석 처리 부분 연결 예정
useEffect(() => {
// otherInfo 상태가 변경될 때마다 isInfoModified 상태를 설정
if (otherInfo !== selectedPlace.otherInfo) {
setIsInfoModified(true);
} else {
setIsInfoModified(false);
}
}, [otherInfo, selectedPlace]);

const handleSaveClick = async () => {
// try {
// const response = await axios.post('/api/map/marker/{court_id}/info/save', {
// placeId: selectedPlace.id,
// otherInfo,
// });
// console.log('데이터 저장 성공:', response.data);
// setIsInfoModified(false); // 저장 후 isInfoModified를 false로 설정하여 저장 버튼 숨김
// } catch (error) {
// console.error('데이터 저장 실패:', error);
// }
};

const handleOtherInfoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setOtherInfo(e.target.value);
};

const handlePhoneClick = () => {
if (selectedPlace.phone) {
if (selectedPlace.phoneNum) {
const confirmDial = window.confirm(
`이 전화번호로 연결하시겠습니까? ${selectedPlace.phone}`
`이 전화번호로 연결하시겠습니까? ${selectedPlace.phoneNum}`
);
if (confirmDial) {
window.location.href = `tel:${selectedPlace.phone}`;
window.location.href = `tel:${selectedPlace.phoneNum}`;
}
}
};

const handleCopyAddress = async () => {
if (selectedPlace.address) {
try {
await navigator.clipboard.writeText(selectedPlace.address);
alert('주소가 복사되었습니다.');
} catch (error) {
console.error('주소 복사 중 오류 발생:', error);
alert('주소를 복사하는 데 실패했습니다.');
}
}
};

return (
<div
className={`absolute inset-0 z-40 m-auto h-fit w-full max-w-md rounded-lg border-1 bg-background
shadow-md transition-all duration-300 ease-in-out ${isVisible ? '' : 'hidden'}`}
className={`absolute inset-0 z-40 m-auto h-fit w-full max-w-md rounded-lg bg-background shadow-md
transition-all duration-300 ease-in-out ${isVisible ? '' : 'hidden'}`}
>
<div className="relative max-h-[85vh] w-full overflow-auto">
<div className="relative h-28 w-full">
<div className="relative max-h-[96vh] w-full overflow-auto rounded-lg text-sm">
<div className="relative h-48 w-full">
<Image
layout="fill"
objectFit="contain"
alt="농구장 사진"
src="/images/court.svg"
src="/images/han-river-park-court.png"
/>
{isInfoModified && (
<Button
size="sm"
className="bg-gradient absolute right-9 top-3"
onClick={handleSaveClick}
aria-label="Save"
>
저장
</Button>
)}
<Button
isIconOnly
className="bg-gradient absolute right-2 top-2"
onClick={onClose}
aria-label="Close"
>
<IoIosClose size={22} />
<IoIosClose size={30} className="text-gray-600" />
</Button>
</div>
<div className="mb-5 px-8">
<div className="my-4 flex justify-between">
<div>
<h2 className="text-lg font-semibold">
{selectedPlace.place_name}
</h2>
<p>{selectedPlace.address_name}</p>
<div className="mb-5 px-8 py-4">
<div className="flex justify-between">
<h2 className="mx-1 text-xl font-bold">
{selectedPlace.courtName}
</h2>
<Button
radius="full"
size="sm"
variant="bordered"
startContent={<PiChatsCircle />}
className="border-1"
aria-label="시설 채팅 바로가기"
>
시설 채팅
</Button>
</div>
<div className="my-4 flex w-full justify-center gap-3">
<Button
color="primary"
variant="bordered"
startContent={<FaRegDotCircle />}
radius="full"
className="border-1"
>
출발
</Button>
<Button
color="primary"
radius="full"
startContent={<FaLocationDot />}
>
도착
</Button>
</div>
<hr className="w-90 my-4 h-px bg-gray-300" />
<div className="my-4 flex flex-col gap-4">
<div className="flex gap-2 align-middle">
<FaLocationDot
size={16}
className="dark:text-gray-20 text-gray-400"
/>
<span>{selectedPlace.address}</span>
<button type="button" onClick={handleCopyAddress}>
<span className="text-blue-500">복사</span>
</button>
</div>
<div className="flex gap-3">
{selectedPlace.phone && (
<FaPhone
size={20}
onClick={handlePhoneClick}
style={{ cursor: 'pointer' }}
/>
)}
<PiChatsCircle size={24} />
<div className="flex gap-2 align-middle">
<FaClock size={14} className="text-gray-400 dark:text-gray-200" />
<span>
개방 시간:{' '}
<span className="text-rose-400">
{selectedPlace.openingHours === true ? '24시간' : '제한'}
</span>
</span>
</div>
<div className="flex gap-2 align-middle">
<FaPhoneAlt
size={15}
className="pointer-events-auto text-gray-400 dark:text-gray-200"
onClick={handlePhoneClick}
/>
<span>
{selectedPlace.phoneNum ? selectedPlace.phoneNum : '-'}
</span>
</div>
<div className="flex gap-2 align-middle">
<FeeIcon className="text-gray-400 dark:text-gray-200" />
<span className="text-info text-blue-500">
이용료: {selectedPlace.fee === true ? '유료' : '무료'}
</span>
</div>
</div>

<div className="flex flex-col gap-4">
<div className="flex gap-4 md:flex-nowrap">
<Input
type="text"
isReadOnly
labelPlacement="outside"
label="코트 종류"
defaultValue="우레탄"
variant="bordered"
className="text-b max-w-xs"
<div className="flex gap-2 align-middle">
<WebsiteIcon className="text-gray-400 dark:text-gray-200" />
<span className="text-blue-500">
{selectedPlace.website !== null ? selectedPlace.website : '-'}
</span>
</div>
<div className="flex gap-2 align-middle">
<CourtIcon className="text-gray-400 dark:text-gray-200" />
<span className="font-medium">
코트 종류: {selectedPlace.courtType}
</span>
</div>
<div className="flex gap-2 align-middle">
<CourtIcon className="text-gray-400 dark:text-gray-200" />
<span className="font-medium">
코트 사이즈: {selectedPlace.courtSize}
</span>
</div>
<div className="flex gap-2 align-middle">
<HoopIcon className="text-gray-400 dark:text-gray-200" />
<span className="font-medium">
골대 수: {selectedPlace.hoopCount}
</span>
</div>
<div className="flex gap-2 align-middle">
<FaLightbulb
size={17}
className="text-gray-400 dark:text-gray-200"
/>
<Input
type="text"
isReadOnly
labelPlacement="outside"
label="코트 사이즈"
defaultValue="풀코트"
variant="bordered"
className="max-w-xs"
<span>
야간 조명:{' '}
{selectedPlace.nightLighting === true ? '있음' : '없음'}
</span>
</div>
<div className="flex gap-2 align-middle">
<FaParking
size={17}
className="text-gray-400 dark:text-gray-200"
/>
<span>
주차: {selectedPlace.parkingAvailabl === true ? '가능' : '불가'}
</span>
</div>

<Input
isReadOnly
labelPlacement="outside"
variant="bordered"
type="number"
label="골대 수"
defaultValue="4"
/>
<RadioGroup
isReadOnly
defaultValue="있음"
label="야간 조명"
orientation="horizontal"
>
<Radio value="있음">있음</Radio>
<Radio value="없음">없음</Radio>
</RadioGroup>
<RadioGroup
isReadOnly
defaultValue="24시"
label="개방 시간"
orientation="horizontal"
>
<Radio value="제한">제한</Radio>
<Radio value="24시">24시</Radio>
</RadioGroup>
<RadioGroup
isReadOnly
defaultValue="무료"
label="사용료"
orientation="horizontal"
>
<Radio value="무료">무료</Radio>
<Radio value="유료">유료</Radio>
</RadioGroup>
<RadioGroup
isReadOnly
defaultValue="불가능"
label="주차여부"
orientation="horizontal"
>
<Radio value="가능">가능</Radio>
<Radio value="불가능">불가능</Radio>
</RadioGroup>
<div className="w-full">
<Textarea
variant="bordered"
label="기타 정보"
defaultValue={otherInfo}
value={otherInfo}
onChange={handleOtherInfoChange}
/>
<div className="flex gap-2 align-middle text-sm">
<FaTag size={17} className="text-gray-400 dark:text-gray-200" />
<span className="rounded-sm bg-gray-100 px-1 text-gray-500 dark:bg-gray-300 dark:text-gray-600">
{selectedPlace.indoorOutdoor}
</span>
<ul className="flex gap-2">
{selectedPlace.convenience.map((tag: string, idx: number) => (
<li
// eslint-disable-next-line react/no-array-index-key
key={idx}
Copy link
Contributor

Choose a reason for hiding this comment

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

DB에 저장될 때 배열로 저장되는 건 웬만하면 id 넣어주고 그 id 값으로 key를 주는 게 좋긴 할 것 같은데.. 사실 태그는 idx로 줘도 전혀 문제될 게 없긴 해서 이 부분이 애매하긴 하네요.. 이건 eslint 무시하고 하는 걸로 하실건가요??

Copy link
Contributor Author

Choose a reason for hiding this comment

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

jsx-eslint/eslint-plugin-react#1123

단순 배열 나열로 idx로 줘도 문제가 없을 것 같아서 그렇게 했습니다!

className="rounded-sm bg-gray-100 px-1 text-gray-500 dark:bg-gray-300 dark:text-gray-600"
>
<span>{tag}</span>
</li>
))}
</ul>
</div>
<div className="flex gap-2 align-middle">
<InfoIcon className="text-gray-400 dark:text-gray-200" />
<span className="text-sm">{selectedPlace.additionalInfo}</span>
</div>
</div>
</div>
Expand Down