Skip to content

Commit

Permalink
Merge pull request #49 from team-joytas/feature/new-course-plan
Browse files Browse the repository at this point in the history
  • Loading branch information
erica0321 authored Jan 12, 2025
2 parents 9f0aa21 + 26321d9 commit c539f0e
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 223 deletions.
160 changes: 160 additions & 0 deletions src/app/components/(course)/CoursePlanNewLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
'use client'

import { ChevronLeft, Plus } from 'lucide-react'
import { useRouter } from 'next/navigation'
import Spacer from '@components/(layout)/Spacer'
import { Place } from '@components/SearchPlace'
import SearchPlace from '@components/SearchPlace'
import SelectCategories from '@components/SelectCategories'
import { useEffect, useState } from 'react'
import { Input } from 'antd'
import SortableList from '@components/SortableList'
import getData from '@/app/schedules/getData'
import KakaoMap from '@components/KakaoMap'
import type { DatePickerProps } from 'antd'
import { DatePicker } from 'antd'
import RegionCascader from '@components/RegionCascader'

const LAYOUT_TYPE = {
course: 'course' as const,
plan: 'plan' as const,
}

type LayoutType = keyof typeof LAYOUT_TYPE

export default function CoursePlanNewLayout({ type }: { type: LayoutType }) {
const router = useRouter()
const [places, setPlaces] = useState<Place[]>([])
const [selectedRegion, setSelectedRegion] = useState<string>('')
const [openSearchPlace, setOpenSearchPlace] = useState<boolean>(false)

const pageType = type === LAYOUT_TYPE.course ? '코스' : '플랜'

useEffect(() => {
const data = getData()
setPlaces(data.courses[0].places)
}, [])

const onChange: DatePickerProps['onChange'] = (date, dateString) => {
console.log('날짜 선택')
}

const onChangePlaces = (place: Place) => {
setPlaces((prevPlaces) => [...prevPlaces, place])
}

const headerTitle =
type === LAYOUT_TYPE.course
? '나만의 코스 작성하기'
: '좋아하는 장소로 채우는 나의 플랜'

return (
<div className='relative h-100% flex flex-col'>
<Header title={headerTitle} onBack={() => router.back()} />
<Spacer height={25} />
<Section title={`${pageType} 제목을 만들어주세요.`} padding>
<Input
allowClear
maxLength={20}
className='rounded-full h-[35px] border-0 bg-bright-gray'
/>
</Section>
<Divider />
<Section title={`${pageType} 지역을 선택하세요.`} padding>
<RegionCascader
placeholder='지역을 선택해주세요.'
setSelectedRegion={setSelectedRegion}
/>
</Section>
<Divider />
<Section title={`${pageType} 장소를 선택하세요.`}>
<div className='px-[20px] w-full flex flex-col gap-[15px] padding'>
{places.length > 0 && <KakaoMap places={places} id={1} />}
<SortableList places={places} setPlaces={setPlaces} />
<button
onClick={() => setOpenSearchPlace(true)}
className='w-full h-[40px] text-[15px] cursor-pointer rounded-full flex items-center justify-center bg-container-light-blue'
>
<Plus size={20} strokeWidth={3} stroke={'#ffffff'} />
</button>
</div>
{openSearchPlace && (
<SearchPlace
setOpenSearchPlace={setOpenSearchPlace}
onChangePlaces={onChangePlaces}
/>
)}
</Section>
<Divider />
<Section title={`${pageType} 설명을 적어주세요.`} padding>
<Input.TextArea allowClear showCount autoSize maxLength={200} />
</Section>
<Divider />
<Section title='방문 날짜를 등록하세요.' padding>
<DatePicker
onChange={onChange}
allowClear
placeholder='날짜를 선택해주세요.'
/>
</Section>
<Divider />
<Section title='관련 태그를 눌러 주세요.' padding>
<SelectCategories />
</Section>
<Spacer height={25} />
<button
className={`w-full text-[12px] h-[54px] flex items-center justify-center bg-blue-100 text-white ${
places.length === 0 ? 'cursor-default' : 'bg-blue-800 bg-opacity-50'
}`}
disabled={places.length === 0}
>
완료
</button>
</div>
)
}

function Header({ title, onBack }: { title: string; onBack: () => void }) {
return (
<header className='max-w-[375px] relative bg-white w-full h-[55px] px-[20px] min-h-[55px] flex justify-between items-center border-b-[1px] border-b-header-line'>
<button onClick={onBack}>
<ChevronLeft size={24} color='black' strokeWidth={1.5} />
</button>
<p className='font-semibold text-[17px]'>{title}</p>
<div className='w-[24px] h-[24px]'></div>
</header>
)
}

function Section({
title,
children,
padding,
}: {
title: string
children: React.ReactNode
padding?: boolean
}) {
return (
<div
className={`w-full flex flex-col gap-[15px] ${
padding ? 'px-[20px]' : ''
}`}
>
<span className={`text-main font-semibold ${padding ? '' : 'px-[20px]'}`}>
{title}
</span>
{children}
</div>
)
}

function Divider() {
return (
<>
<Spacer height={25} />
<Spacer height={8} className='bg-bright-gray' />
<Spacer height={25} />
</>
)
}
2 changes: 1 addition & 1 deletion src/app/components/RegionCascader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default function RegionCascader({
size='large'
showSearch={{ filter }}
style={{
width: '300px',
width: '100%',
}}
expandTrigger='hover'
/>
Expand Down
101 changes: 62 additions & 39 deletions src/app/components/SearchPlace.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useEffect, useState } from 'react'
import { Input } from 'antd'
import { ChevronLeft } from 'lucide-react'
import Spacer from '@components/(layout)/Spacer'

export interface Place {
id: string
Expand All @@ -21,18 +23,29 @@ interface Meta {
}

interface SearchPlaceProps {
onOpenDrawer?: (open: boolean) => void
onSelectPlace?: (place: Place) => void
setOpenSearchPlace: (open: boolean) => void
onChangePlaces: (place: Place) => void
}

export default function SearchPlace({
onOpenDrawer,
onSelectPlace,
setOpenSearchPlace,
onChangePlaces,
}: SearchPlaceProps) {
const [results, setResults] = useState<Place[]>([])
const [meta, setMeta] = useState<Meta>() // TODO: 페이지네이션 추가 필요
const [inputValue, setInputValue] = useState('')

useEffect(() => {
getResult(inputValue)
}, [inputValue])

useEffect(() => {
document.body.style.overflow = 'hidden'
return () => {
document.body.style.overflow = 'unset'
}
}, [])

const getResult = async (value: string) => {
if (!value) return

Expand All @@ -50,46 +63,56 @@ export default function SearchPlace({
}

const selectPlace = (place: Place) => {
if (onOpenDrawer && onSelectPlace) {
onOpenDrawer(false)
onSelectPlace(place)
if (onChangePlaces) {
setOpenSearchPlace(false)
onChangePlaces(place)
setInputValue('')
}
}

useEffect(() => {
getResult(inputValue)
}, [inputValue])

return (
<div className='max-w-[375px] w-full m-auto'>
<div className='flex flex-col gap-[10px]'>
<span className='text-[15px] font-semibold'>장소명</span>
<Input.Search
placeholder='장소 이름을 입력해주세요.'
className='w-full'
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
</div>
<div className='w-full mt-[20px] mb-[20px] h-[2px] bg-gray-100' />
<div className='flex flex-col gap-[5px] justify-start items-center'>
{results.map((result) => {
return (
<div
key={result.id}
className='text-[12px] border-blue-100 rounded-[5px] border-[1px] flex flex-col w-full gap-[5px] px-[15px] py-[8px] cursor-pointer'
onClick={() => {
selectPlace(result)
}}
>
<span className='text-[13px] font-bold'>{result.place_name}</span>
<span className='text-[10px] text-gray-600'>
{result.address_name}
</span>
</div>
)
})}
<div className='fixed top-0 transform z-[1000] w-full max-w-[375px] h-full bg-white'>
<header className=' bg-white h-[55px] px-[20px] min-h-[55px] flex justify-between items-center border-b-[1px] border-b-header-line'>
<button onClick={() => setOpenSearchPlace(false)}>
<ChevronLeft size={24} color='black' strokeWidth={1.5} />
</button>
<p className='font-semibold text-[17px]'>장소 추가하기</p>
<div className='w-[24px] h-[24px]'></div>
</header>
<Spacer height={18} />
<div className='px-[20px] w-full flex flex-col gap-[15px]'>
<div className='flex flex-col gap-[10px]'>
<span className='text-main font-semibold'>장소명</span>
<Input.Search
placeholder='장소 이름을 입력해주세요.'
className='w-full'
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
</div>
<div className='w-full mt-[20px] h-[2px] bg-gray-100' />
<div className='justify-start items-center overflow-y-auto h-[calc(100vh-200px)]'>
<div className='h-fit w-full flex flex-col gap-[5px]'>
{results.map((result) => {
return (
<div
key={result.id}
className='text-[12px] border-blue-100 rounded-[5px] border-[1px] flex flex-col w-full gap-[5px] px-[15px] py-[8px] cursor-pointer'
onClick={() => {
selectPlace(result)
}}
>
<span className='text-[13px] font-bold'>
{result.place_name}
</span>
<span className='text-[10px] text-gray-600'>
{result.address_name}
</span>
</div>
)
})}
</div>
</div>
</div>
</div>
)
Expand Down
32 changes: 32 additions & 0 deletions src/app/components/SelectCategories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useState } from 'react'
import { categories } from '@/types/Categories'

export default function SelectCategories() {
const [clickedCategory, setClickedCategory] = useState<number[]>([])

const handleCategoryClick = (id: number) => {
setClickedCategory((prev) =>
prev.includes(id)
? prev.filter((categoryId) => categoryId !== id)
: [...prev, id]
)
}

return (
<div className='inline-flex flex-wrap gap-[5px]'>
{categories.map((category) => (
<button
key={category.id}
className={`text-middle py-[5px] px-[10px] rounded-full border-[1px] border-container-blue transition-all duration-100 cursor-pointer ${
clickedCategory.includes(category.id)
? 'bg-container-blue text-white'
: 'bg-white text-container-blue'
}`}
onClick={() => handleCategoryClick(category.id)}
>
{category.value}
</button>
))}
</div>
)
}
19 changes: 9 additions & 10 deletions src/app/components/SortableItem.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Button } from 'antd'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { Place } from '@/app/components/SearchPlace'
import { AlignJustify } from 'lucide-react'
import { AlignJustify, X, Image } from 'lucide-react'

interface SortableItemProps {
id: string
Expand Down Expand Up @@ -33,29 +32,29 @@ export default function SortableItem({
transition,
touchAction: 'none',
}}
className='flex items-center justify-between gap-[10px] h-[40px] px-[10px] py-[10px] border text-[15px] rounded-[5px] bg-white'
className='flex items-center justify-between gap-[10px] h-[40px] px-[10px] py-[10px] text-[15px] rounded-full bg-bright-gray'
>
<div className='flex items-center gap-[10px]'>
<div {...attributes} {...listeners} ref={setNodeRef}>
<AlignJustify size={20} strokeWidth={1.5} />
<AlignJustify size={20} strokeWidth={1.5} stroke={'#5A59F2'} />
</div>
<span className='text-[13px]'>{place.place_name}</span>
</div>
<div className='flex gap-[5px]'>
{onEdit && (
<Button
<button
className='text-[10px] border-0 h-[20px] w-[20px]'
onClick={() => handleEdit(place.id)}
>
수정
</Button>
<Image size={20} stroke={'#A9A9A9'} strokeWidth={1.5} />
</button>
)}
<Button
<button
className='text-[10px] shadow-none border-0 h-[20px] w-[30px]'
onClick={() => onDelete(place.id)}
>
X
</Button>
<X size={20} stroke={'#A9A9A9'} strokeWidth={1.5} />
</button>
</div>
</div>
)
Expand Down
Loading

0 comments on commit c539f0e

Please sign in to comment.