feat: Implement survey-sale, inquiry UI base components

- 조사매물, 1:1 문의 목록 더보기 버튼 구현
- 조사매물, 1:1 문의 목록, 상세 페이지, 작성 페이지 샘플 구현
This commit is contained in:
Dayoung 2025-04-29 10:52:49 +09:00
parent 37a40989dd
commit 08d99fb4e7
15 changed files with 416 additions and 44 deletions

66
src/api/survey.ts Normal file
View File

@ -0,0 +1,66 @@
import { axiosInstance } from '@/libs/axios'
export interface Survey {
id: number
title: string
content: string
created_at: string
updated_at: string
checked: string[]
otherText: string
}
export const surveyApi = {
create: async (data: Survey): Promise<Survey> => {
try {
const response = await axiosInstance.post<Survey>('/api/survey', data)
return response.data
} catch (error) {
console.error('Error creating survey:', error)
throw error
}
},
getList: async (): Promise<Survey[]> => {
try {
const response = await axiosInstance.get<Survey[]>('/api/survey')
return response.data
} catch (error) {
console.error('Error fetching survey list:', error)
return []
}
},
getDetail: async (id?: number): Promise<Survey> => {
try {
if (id) {
const response = await axiosInstance.get<Survey>(`/api/survey/${id}`)
return response.data
} else {
return {} as Survey
}
} catch (error) {
console.error('Error fetching survey detail:', error)
throw error
}
},
update: async (id: number, data: Survey): Promise<Survey> => {
try {
const response = await axiosInstance.put<Survey>(`/api/survey/${id}`, data)
return response.data
} catch (error) {
console.error('Error updating survey:', error)
throw error
}
},
delete: async (id: number): Promise<void> => {
try {
await axiosInstance.delete(`/api/survey/${id}`)
} catch (error) {
console.error('Error deleting survey:', error)
throw error
}
},
}

View File

@ -0,0 +1,28 @@
import { NextResponse } from 'next/server'
export async function GET(request: Request, { params }: { params: { id: string } }) {
const { id } = params
// @ts-ignore
const survey = await prisma.SD_SERVEY_SALES.findUnique({
where: { id: parseInt(id) },
})
return NextResponse.json(survey)
}
export async function PUT(request: Request, { params }: { params: { id: string } }) {
const { id } = params
const body = await request.json()
// @ts-ignore
const survey = await prisma.SD_SERVEY_SALES.update({
where: { id: parseInt(id) },
data: body,
})
return NextResponse.json(survey)
}
export async function DELETE(request: Request, { params }: { params: { id: string } }) {
const { id } = params
// @ts-ignore
await prisma.SD_SERVEY_SALES.delete({ where: { id: parseInt(id) } })
return NextResponse.json({ message: 'Survey deleted successfully' })
}

View File

@ -0,0 +1,16 @@
import { NextResponse } from 'next/server'
export async function GET() {
// @ts-ignore
const surveys = await prisma.SD_SERVEY_SALES.findMany()
return NextResponse.json(surveys)
}
export async function POST(request: Request) {
const body = await request.json()
// @ts-ignore
const survey = await prisma.SD_SERVEY_SALES.create({
data: body,
})
return NextResponse.json(survey)
}

View File

@ -0,0 +1,9 @@
import SurveySaleDetail from '@/components/survey/SurveySaleDetail'
export default function SurveyDetailPage() {
return (
<div>
<SurveySaleDetail />
</div>
)
}

10
src/app/survey/page.tsx Normal file
View File

@ -0,0 +1,10 @@
import SurveySaleList from '@/components/survey/SurveySaleList'
export default function SurveyPage() {
return (
<div>
<SurveySaleList />
</div>
)
}

View File

@ -0,0 +1,9 @@
import SurveySaleWriteForm from '@/components/survey/SurveySaleWriteForm'
export default function SurveyWritePage() {
return (
<div>
<SurveySaleWriteForm />
</div>
)
}

View File

@ -0,0 +1,11 @@
'use client'
interface LoadMoreButtonProps {
hasMore: boolean
onLoadMore: () => void
onScrollToTop: () => void
}
export default function LoadMoreButton({ hasMore, onLoadMore, onScrollToTop }: LoadMoreButtonProps) {
return <div>{hasMore ? <button onClick={onLoadMore}>Load More</button> : <button onClick={onScrollToTop}>Scroll to Top</button>}</div>
}

View File

@ -3,7 +3,8 @@
import { Search } from 'lucide-react'
import { useRouter } from 'next/navigation'
export default function InquirySearch({ handleSearch }: { handleSearch: (e: React.ChangeEvent<HTMLInputElement>) => void }) {
export default function InquiryFilter({ handleSearch }: { handleSearch: (e: React.ChangeEvent<HTMLInputElement>) => void }) {
const router = useRouter()
return (
<div>

View File

@ -1,7 +1,8 @@
'use client'
import { useState } from 'react'
import InquiryItems from './InquiryItems'
import InquirySearch from './InquirySearch'
import InquiryFilter from './InquiryFilter'
import LoadMoreButton from '../LoadMoreButton'
const inquiryDummyData = [
{
@ -120,6 +121,7 @@ export default function InquiryList() {
const [isMyPostsOnly, setIsMyPostsOnly] = useState(false)
const [category, setCategory] = useState('')
const [search, setSearch] = useState('')
const [hasMore, setHasMore] = useState(inquiryDummyData.length > 5)
const inquriyData = () => {
if (isMyPostsOnly) {
@ -135,7 +137,9 @@ export default function InquiryList() {
}
const handleLoadMore = () => {
setVisibleItems((prev) => Math.min(prev + 5, inquriyData().length))
const newVisibleItems = Math.min(visibleItems + 5, inquriyData().length)
setVisibleItems(newVisibleItems)
setHasMore(newVisibleItems < inquriyData().length)
}
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -148,7 +152,7 @@ export default function InquiryList() {
return (
<div className="flex flex-col gap-2">
<InquirySearch handleSearch={handleSearch} />
<InquiryFilter handleSearch={handleSearch} />
<div className="flex items-center gap-2">
<input type="checkbox" id="myPosts" checked={isMyPostsOnly} onChange={(e) => setIsMyPostsOnly(e.target.checked)} />
<label htmlFor="myPosts">my posts</label>
@ -161,12 +165,7 @@ export default function InquiryList() {
</select>
<span>total {inquriyData().length}</span>
<InquiryItems inquiryData={inquriyData().slice(0, visibleItems)} />
{visibleItems < inquriyData().length ? (
<button onClick={handleLoadMore}>more</button>
) : (
<button onClick={handleScrollToTop}>top</button>
)}
<LoadMoreButton hasMore={hasMore} onLoadMore={handleLoadMore} onScrollToTop={handleScrollToTop} />
</div>
)
}

View File

@ -28,15 +28,13 @@ export default function InquiryWriteForm() {
setFormData({ ...formData, file: formData.file.filter((f) => f !== fileToDelete) })
}
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const handleSubmit = () => {
console.log('submit: ', formData)
router.push(`/inquiry`)
// router.push(`/inquiry`)
}
return (
<div>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="category">category</label>
<select id="category" onChange={(e) => setFormData({ ...formData, category: e.target.value })}>
@ -68,8 +66,7 @@ export default function InquiryWriteForm() {
))}
</div>
</div>
<button type="submit">submit</button>
</form>
<button onClick={handleSubmit}>submit</button>
</div>
)
}

View File

@ -0,0 +1,20 @@
import { Search } from 'lucide-react'
import { useRouter } from 'next/navigation'
export default function SurveyFilter({ handleSearch, handleMyPosts }: { handleSearch: (e: React.ChangeEvent<HTMLInputElement>) => void, handleMyPosts: () => void }) {
const router = useRouter()
return (
<div>
<button onClick={() => router.push('/survey/write')}>write survey {'>'}</button>
<div className="flex items-center gap-2">
<input type="text" placeholder="Search" onChange={handleSearch} />
<button>
<Search />
</button>
</div>
<div className="flex items-center gap-2">
<button onClick={handleMyPosts}>my posts</button>
</div>
</div>
)
}

View File

@ -0,0 +1,4 @@
export default function SurveySaleDetail() {
return <div>SurveySaleDetail</div>
}

View File

@ -0,0 +1,54 @@
'use client'
import { useServey } from '@/hooks/useSurvey'
import LoadMoreButton from '@/components/LoadMoreButton'
import { useState } from 'react'
import SurveyFilter from './SurveyFilter'
export default function SurveySaleList() {
const { surveyList, isLoadingSurveyList } = useServey()
const [search, setSearch] = useState('')
const [isMyPostsOnly, setIsMyPostsOnly] = useState(false)
const [hasMore, setHasMore] = useState(surveyList.length > 5)
const [visibleItems, setVisibleItems] = useState(5)
const surveyData = () => {
if (search.trim().length > 0) {
return surveyList.filter((survey) => survey.title.includes(search))
}
return surveyList
}
const handleLoadMore = () => {
const newVisibleItems = Math.min(visibleItems + 5, surveyData().length)
setVisibleItems(newVisibleItems)
setHasMore(newVisibleItems < surveyData().length)
}
const handleScrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value)
}
if (isLoadingSurveyList) {
return <div>Loading...</div>
}
return (
<div>
<SurveyFilter handleSearch={handleSearch} handleMyPosts={() => setIsMyPostsOnly(!isMyPostsOnly)} />
{surveyList.slice(0, visibleItems).map((survey) => (
<div key={survey.id}>
<h2>{survey.title}</h2>
<p>{survey.content}</p>
<p>{survey.created_at}</p>
<p>{survey.updated_at}</p>
</div>
))}
<LoadMoreButton hasMore={hasMore} onLoadMore={handleLoadMore} onScrollToTop={handleScrollToTop} />
</div>
)
}

View File

@ -0,0 +1,97 @@
'use client'
import { Survey } from '@/api/survey'
import { useState } from 'react'
import { useServey } from '@/hooks/useSurvey'
import { useRouter } from 'next/navigation'
export default function SurveySaleWriteForm() {
const router = useRouter()
const { createSurvey, isCreatingSurvey } = useServey()
const [formData, setFormData] = useState<Survey>({
id: 0,
title: '',
content: '',
created_at: '',
updated_at: '',
checked: [],
otherText: '',
})
const checkboxOptions = ['A', 'B', 'C', 'D', '기타']
const handleSubmit = () => {
console.log('form:: ', formData)
// createSurvey(formData)
// router.push('/survey')
}
const handleCheckboxChange = (option: string) => {
if (formData.checked.includes(option)) {
setFormData({
...formData,
checked: formData.checked.filter((item) => item !== option),
...(option === '기타' ? { otherText: '' } : {}),
})
} else {
setFormData({
...formData,
checked: [...formData.checked, option],
})
}
}
const handleOtherTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({
...formData,
otherText: e.target.value,
checked: formData.checked.includes('기타') ? formData.checked : [...formData.checked, '기타'],
})
}
if (isCreatingSurvey) {
return <div>Loading...</div>
}
return (
<div>
<div>
<label htmlFor="title">title</label>
<input type="text" id="title" value={formData.title} onChange={(e) => setFormData({ ...formData, title: e.target.value })} />
</div>
<div>
<label htmlFor="content">content</label>
<textarea id="content" value={formData.content} onChange={(e) => setFormData({ ...formData, content: e.target.value })} />
</div>
<div>
{checkboxOptions.map((option) => (
<div key={option}>
<input
type="checkbox"
id={option}
checked={formData.checked.includes(option)}
onChange={() => handleCheckboxChange(option)}
tabIndex={0}
aria-label={option}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') handleCheckboxChange(option)
}}
/>
<label htmlFor={option} className="cursor-pointer">
{option}
</label>
{option === '기타' && formData.checked.includes('기타') && (
<input type="text" value={formData.otherText || ''} onChange={handleOtherTextChange} placeholder="기타 입력" aria-label="기타 입력" />
)}
</div>
))}
</div>
<div>
<button onClick={handleSubmit} className="cursor-pointer">
save
</button>
</div>
</div>
)
}

51
src/hooks/useSurvey.ts Normal file
View File

@ -0,0 +1,51 @@
import { Survey, surveyApi } from '@/api/survey'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
export function useServey(id?: number) {
const queryClient = useQueryClient()
const { data: surveyList, isLoading: isLoadingSurveyList } = useQuery({
queryKey: ['survey', 'list'],
queryFn: () => surveyApi.getList(),
})
const { data: surveyDetail, isLoading: isLoadingSurveyDetail } = useQuery({
queryKey: ['survey', id],
queryFn: () => surveyApi.getDetail(id),
enabled: !!id,
})
const { mutate: createSurvey, isPending: isCreatingSurvey } = useMutation({
mutationFn: (survey: Survey) => surveyApi.create(survey),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['survey', 'list'] })
},
})
const { mutate: updateSurvey, isPending: isUpdatingSurvey } = useMutation({
mutationFn: (survey: Survey) => surveyApi.update(survey.id, survey),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['survey', 'list'] })
},
})
const { mutate: deleteSurvey, isPending: isDeletingSurvey } = useMutation({
mutationFn: (id: number) => surveyApi.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['survey', 'list'] })
},
})
return {
surveyList: surveyList ?? [],
surveyDetail: surveyDetail ?? {},
isLoadingSurveyList,
isLoadingSurveyDetail,
isCreatingSurvey,
isUpdatingSurvey,
isDeletingSurvey,
createSurvey,
updateSurvey,
deleteSurvey,
}
}