feat: implement SurveyFilteringOptions to global

- 조사매물 검색 조건 zustand 통해 전역으로 관리하도록 구현
This commit is contained in:
Dayoung 2025-05-08 16:58:14 +09:00
parent 68408eb3c9
commit dfd5ba419b
6 changed files with 260 additions and 134 deletions

View File

@ -10,10 +10,54 @@ export async function POST(request: Request) {
return NextResponse.json(res) return NextResponse.json(res)
} }
export async function GET() { export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const keyword = searchParams.get('keyword')
const searchOption = searchParams.get('searchOption')
const isMySurvey = searchParams.get('isMySurvey')
const sort = searchParams.get('sort')
const searchOptions = ['building_name', 'representative', 'store', 'construction_point', 'customer_name', 'post_code', 'address', 'address_detail']
try { try {
const where: any = {}
if (isMySurvey !== null && isMySurvey !== undefined) {
where.representative = isMySurvey
}
if (keyword && keyword.trim() !== '' && searchOption) {
if (searchOption === 'all') {
where.OR = [];
if (keyword.match(/^\d+$/) || !isNaN(Number(keyword))) {
where.OR.push({
id: {
equals: Number(keyword),
},
})
}
where.OR.push(
...searchOptions.map((field) => ({
[field]: {
contains: keyword,
},
}))
)
} else if (searchOptions.includes(searchOption)) {
where[searchOption] = {
contains: keyword,
}
} else if (searchOption === 'id') {
where[searchOption] = {
equals: Number(keyword),
}
}
}
// @ts-ignore // @ts-ignore
const res = await prisma.SD_SERVEY_SALES_BASIC_INFO.findMany() const res = await prisma.SD_SERVEY_SALES_BASIC_INFO.findMany({
where,
orderBy: sort === 'created' ? { created_at: 'desc' } : { updated_at: 'desc' },
})
return NextResponse.json(res) return NextResponse.json(res)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@ -21,21 +65,9 @@ export async function GET() {
} }
} }
// export async function GET(request: Request) {
// // @ts-ignore
// const res = await prisma.SD_SERVEY_SALES_BASIC_INFO.findMany({
// include: {
// detail_info: true,
// },
// })
// return NextResponse.json(res)
// }
export async function PUT(request: Request) { export async function PUT(request: Request) {
const body = await request.json() const body = await request.json()
console.log('🚀 ~ PUT ~ body:', body)
const detailInfo = { ...body.detail_info, basic_info_id: body.id } const detailInfo = { ...body.detail_info, basic_info_id: body.id }
console.log('🚀 ~ PUT ~ detailInfo:', detailInfo)
// @ts-ignore // @ts-ignore
const res = await prisma.SD_SERVEY_SALES_DETAIL_INFO.create({ const res = await prisma.SD_SERVEY_SALES_DETAIL_INFO.create({
data: detailInfo, data: detailInfo,

View File

@ -52,9 +52,14 @@ export default function DataTable() {
<tr> <tr>
<th></th> <th></th>
<td> <td>
{surveyDetail?.submission_status && surveyDetail?.submission_date {surveyDetail?.submission_status && surveyDetail?.submission_date ? (
? new Date(surveyDetail.submission_date).toLocaleString() <>
: '-'} <div>{new Date(surveyDetail.submission_date).toLocaleString()}</div>
<div> ID </div>
</>
) : (
'-'
)}
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -2,7 +2,7 @@
import LoadMoreButton from '@/components/LoadMoreButton' import LoadMoreButton from '@/components/LoadMoreButton'
import { useServey } from '@/hooks/useSurvey' import { useServey } from '@/hooks/useSurvey'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import SearchForm from './SearchForm' import SearchForm from './SearchForm'
@ -10,33 +10,17 @@ export default function ListTable() {
const router = useRouter() const router = useRouter()
const { surveyList, isLoadingSurveyList } = useServey() const { surveyList, isLoadingSurveyList } = useServey()
const [visibleItems, setVisibleItems] = useState(5) const [visibleItems, setVisibleItems] = useState(10)
const [search, setSearch] = useState('')
const [isMyPostsOnly, setIsMyPostsOnly] = useState(false)
const [hasMore, setHasMore] = useState(false) const [hasMore, setHasMore] = useState(false)
// TODO: 로그인 구현 이후 USERNAME 변경
const username = 'test'
const filteredSurveyList = useMemo(() => {
let filtered = surveyList
if (search.trim().length > 0) {
filtered = filtered.filter((survey) => survey.building_name?.includes(search))
}
if (isMyPostsOnly) {
filtered = filtered.filter((survey) => survey.representative === username)
}
return filtered
}, [surveyList, search, isMyPostsOnly, username])
useEffect(() => { useEffect(() => {
setHasMore(filteredSurveyList.length > visibleItems) setHasMore(surveyList.length > visibleItems)
}, [filteredSurveyList, visibleItems]) }, [surveyList, visibleItems])
const handleLoadMore = () => { const handleLoadMore = () => {
const newVisibleItems = Math.min(visibleItems + 5, filteredSurveyList.length) const newVisibleItems = Math.min(visibleItems + 10, surveyList.length)
setVisibleItems(newVisibleItems) setVisibleItems(newVisibleItems)
setHasMore(newVisibleItems < filteredSurveyList.length) setHasMore(newVisibleItems < surveyList.length)
} }
const handleScrollToTop = () => { const handleScrollToTop = () => {
@ -47,26 +31,17 @@ export default function ListTable() {
router.push(`/survey-sale/${id}`) router.push(`/survey-sale/${id}`)
} }
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value)
setVisibleItems(5)
}
const handleMyPostsToggle = () => {
setIsMyPostsOnly((prev) => !prev)
setVisibleItems(5)
}
if (isLoadingSurveyList) { if (isLoadingSurveyList) {
return <div>Loading...</div> return <div>Loading...</div>
} }
return ( return (
<> <>
<SearchForm handleSearch={handleSearchChange} handleMyPosts={handleMyPostsToggle} /> <SearchForm onItemsInit={() => setVisibleItems(10)} />
{surveyList.length > 0 ? (
<div className="sale-frame"> <div className="sale-frame">
<ul className="sale-list-wrap"> <ul className="sale-list-wrap">
{filteredSurveyList.slice(0, visibleItems).map((survey) => ( {surveyList.slice(0, visibleItems).map((survey) => (
<li className="sale-list-item cursor-pointer" key={survey.id} onClick={() => handleDetailClick(survey.id)}> <li className="sale-list-item cursor-pointer" key={survey.id} onClick={() => handleDetailClick(survey.id)}>
<div className="sale-item-bx"> <div className="sale-item-bx">
<div className="sale-item-date-bx"> <div className="sale-item-date-bx">
@ -87,6 +62,11 @@ export default function ListTable() {
<LoadMoreButton hasMore={hasMore} onLoadMore={handleLoadMore} onScrollToTop={handleScrollToTop} /> <LoadMoreButton hasMore={hasMore} onLoadMore={handleLoadMore} onScrollToTop={handleScrollToTop} />
</div> </div>
</div> </div>
) : (
<div>
<p></p>
</div>
)}
</> </>
) )
} }

View File

@ -1,9 +1,25 @@
'use client' 'use client'
import { SEARCH_OPTIONS, SEARCH_OPTIONS_ENUM, useSurveyFilterStore } from '@/store/surveyFilterStore'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useState } from 'react'
export default function SearchForm({ handleSearch, handleMyPosts }: { handleSearch: (e: React.ChangeEvent<HTMLInputElement>) => void, handleMyPosts: () => void }) { export default function SearchForm({ onItemsInit }: { onItemsInit: () => void }) {
const router = useRouter() const router = useRouter()
const { setSearchOption, setSort, setIsMySurvey, setKeyword, isMySurvey, keyword, searchOption, sort } = useSurveyFilterStore()
const [searchKeyword, setSearchKeyword] = useState(keyword)
const username = 'test'
const handleSearch = () => {
if (searchKeyword.trim().length < 2) {
alert('2文字以上入力してください')
return
}
setKeyword(searchKeyword)
onItemsInit()
}
return ( return (
<div className="sale-frame"> <div className="sale-frame">
<div className="sale-form-bx"> <div className="sale-form-bx">
@ -12,33 +28,64 @@ export default function SearchForm({ handleSearch, handleMyPosts }: { handleSear
</button> </button>
</div> </div>
<div className="sale-form-bx"> <div className="sale-form-bx">
<select className="select-form" name="" id=""> <select
<option value=""></option> className="select-form"
<option value=""></option> name="search-option"
<option value=""></option> id="search-option"
<option value=""></option> value={searchOption}
<option value=""></option> onChange={(e) => setSearchOption(e.target.value as SEARCH_OPTIONS_ENUM)}
>
{SEARCH_OPTIONS.map((option) => (
<option key={option.id} value={option.id}>
{option.label}
</option>
))}
</select> </select>
</div> </div>
<div className="sale-form-bx"> <div className="sale-form-bx">
<div className="search-input"> <div className="search-input">
<input type="text" className="search-frame" placeholder="タイトルを入力してください. (2文字以上)" onChange={handleSearch} /> <input
<button className="search-icon"></button> type="text"
className="search-frame"
value={searchKeyword}
placeholder="タイトルを入力してください. (2文字以上)"
onChange={(e) => setSearchKeyword(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleSearch()
}
}}
/>
<button className="search-icon" onClick={handleSearch}></button>
</div> </div>
</div> </div>
<div className="sale-form-bx"> <div className="sale-form-bx">
<div className="check-form-box"> <div className="check-form-box">
<input type="checkbox" id="ch01" onClick={handleMyPosts} /> <input
type="checkbox"
id="ch01"
checked={isMySurvey === username}
onChange={() => {
setIsMySurvey(isMySurvey === username ? null : username)
onItemsInit()
}}
/>
<label htmlFor="ch01"></label> <label htmlFor="ch01"></label>
</div> </div>
</div> </div>
<div className="sale-form-bx"> <div className="sale-form-bx">
<select className="select-form" name="" id=""> <select
<option value=""></option> className="select-form"
<option value=""></option> name="sort-option"
<option value=""></option> id="sort-option"
<option value=""></option> value={sort}
<option value=""></option> onChange={(e) => {
setSort(e.target.value as 'created' | 'updated')
onItemsInit()
}}
>
<option value="created"></option>
<option value="updated"></option>
</select> </select>
</div> </div>
</div> </div>

View File

@ -1,6 +1,7 @@
import { axiosInstance } from '@/libs/axios'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import type { SurveyBasicInfo, SurveyBasicRequest, SurveyDetailInfo, SurveyDetailRequest } from '@/types/Survey' import type { SurveyBasicInfo, SurveyBasicRequest, SurveyDetailInfo, SurveyDetailRequest, SurveyDetailCoverRequest } from '@/types/Survey'
import { axiosInstance } from '@/libs/axios'
import { useSurveyFilterStore } from '@/store/surveyFilterStore'
export function useServey(id?: number): { export function useServey(id?: number): {
surveyList: SurveyBasicInfo[] | [] surveyList: SurveyBasicInfo[] | []
@ -11,18 +12,22 @@ export function useServey(id?: number): {
isUpdatingSurvey: boolean isUpdatingSurvey: boolean
isDeletingSurvey: boolean isDeletingSurvey: boolean
createSurvey: (survey: SurveyBasicRequest) => Promise<number> createSurvey: (survey: SurveyBasicRequest) => Promise<number>
createSurveyDetail: (params: { surveyId: number; surveyDetail: SurveyDetailRequest }) => void createSurveyDetail: (params: { surveyId: number; surveyDetail: SurveyDetailCoverRequest }) => void
updateSurvey: (survey: SurveyBasicRequest) => void updateSurvey: (survey: SurveyBasicRequest) => void
deleteSurvey: () => Promise<boolean> deleteSurvey: () => Promise<boolean>
submitSurvey: () => void submitSurvey: () => void
validateSurveyDetail: (surveyDetail: SurveyDetailRequest) => string validateSurveyDetail: (surveyDetail: SurveyDetailRequest) => string
} { } {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { keyword, searchOption, isMySurvey, sort } = useSurveyFilterStore()
const { data: surveyList, isLoading: isLoadingSurveyList } = useQuery({ const { data: surveyList, isLoading: isLoadingSurveyList } = useQuery({
queryKey: ['survey', 'list'], queryKey: ['survey', 'list', keyword, searchOption, isMySurvey, sort],
queryFn: async () => { queryFn: async () => {
const resp = await axiosInstance.get<SurveyBasicInfo[]>('/api/survey-sales') console.log('keyword, searchOption, isMySurvey, sort:: ', keyword, searchOption, isMySurvey, sort)
const resp = await axiosInstance(null).get<SurveyBasicInfo[]>('/api/survey-sales', {
params: { keyword, searchOption, isMySurvey, sort },
})
return resp.data return resp.data
}, },
}) })
@ -32,7 +37,7 @@ export function useServey(id?: number): {
queryFn: async () => { queryFn: async () => {
if (id === undefined) throw new Error('id is required') if (id === undefined) throw new Error('id is required')
if (id === null) return null if (id === null) return null
const resp = await axiosInstance.get<SurveyBasicInfo>(`/api/survey-sales/${id}`) const resp = await axiosInstance(null).get<SurveyBasicInfo>(`/api/survey-sales/${id}`)
return resp.data return resp.data
}, },
enabled: id !== undefined, enabled: id !== undefined,
@ -40,7 +45,7 @@ export function useServey(id?: number): {
const { mutateAsync: createSurvey, isPending: isCreatingSurvey } = useMutation({ const { mutateAsync: createSurvey, isPending: isCreatingSurvey } = useMutation({
mutationFn: async (survey: SurveyBasicRequest) => { mutationFn: async (survey: SurveyBasicRequest) => {
const resp = await axiosInstance.post<SurveyBasicInfo>('/api/survey-sales', survey) const resp = await axiosInstance(null).post<SurveyBasicInfo>('/api/survey-sales', survey)
return resp.data.id ?? 0 return resp.data.id ?? 0
}, },
onSuccess: (data) => { onSuccess: (data) => {
@ -54,7 +59,7 @@ export function useServey(id?: number): {
mutationFn: async (survey: SurveyBasicRequest) => { mutationFn: async (survey: SurveyBasicRequest) => {
console.log('updateSurvey:: ', survey) console.log('updateSurvey:: ', survey)
if (id === undefined) throw new Error('id is required') if (id === undefined) throw new Error('id is required')
const resp = await axiosInstance.put<SurveyBasicInfo>(`/api/survey-sales/${id}`, survey) const resp = await axiosInstance(null).put<SurveyBasicInfo>(`/api/survey-sales/${id}`, survey)
return resp.data return resp.data
}, },
onSuccess: () => { onSuccess: () => {
@ -66,7 +71,7 @@ export function useServey(id?: number): {
const { mutateAsync: deleteSurvey, isPending: isDeletingSurvey } = useMutation({ const { mutateAsync: deleteSurvey, isPending: isDeletingSurvey } = useMutation({
mutationFn: async () => { mutationFn: async () => {
if (id === null) throw new Error('id is required') if (id === null) throw new Error('id is required')
const resp = await axiosInstance.delete<boolean>(`/api/survey-sales/${id}`) const resp = await axiosInstance(null).delete<boolean>(`/api/survey-sales/${id}`)
return resp.data return resp.data
}, },
onSuccess: () => { onSuccess: () => {
@ -76,8 +81,8 @@ export function useServey(id?: number): {
}) })
const { mutateAsync: createSurveyDetail, isPending: isCreatingSurveyDetail } = useMutation({ const { mutateAsync: createSurveyDetail, isPending: isCreatingSurveyDetail } = useMutation({
mutationFn: async ({ surveyId, surveyDetail }: { surveyId: number; surveyDetail: SurveyDetailRequest }) => { mutationFn: async ({ surveyId, surveyDetail }: { surveyId: number; surveyDetail: SurveyDetailCoverRequest }) => {
const resp = await axiosInstance.post<SurveyDetailInfo>(`/api/survey-sales/${surveyId}`, surveyDetail) const resp = await axiosInstance(null).patch<SurveyDetailInfo>(`/api/survey-sales/${surveyId}`, surveyDetail)
return resp.data return resp.data
}, },
onSuccess: () => { onSuccess: () => {
@ -89,7 +94,9 @@ export function useServey(id?: number): {
const { mutateAsync: submitSurvey } = useMutation({ const { mutateAsync: submitSurvey } = useMutation({
mutationFn: async () => { mutationFn: async () => {
if (id === undefined) throw new Error('id is required') if (id === undefined) throw new Error('id is required')
const resp = await axiosInstance.patch<boolean>(`/api/survey-sales/${id}`) const resp = await axiosInstance(null).patch<boolean>(`/api/survey-sales/${id}`, {
submit: true,
})
return resp.data return resp.data
}, },
onSuccess: () => { onSuccess: () => {
@ -108,27 +115,24 @@ export function useServey(id?: number): {
'waterproof_material', 'waterproof_material',
'insulation_presence', 'insulation_presence',
'structure_order', 'structure_order',
] as const; ] as const
const etcFields = [ const etcFields = ['installation_system', 'construction_year', 'rafter_size', 'rafter_pitch', 'waterproof_material', 'structure_order'] as const
'installation_system',
'construction_year',
'rafter_size',
'rafter_pitch',
'waterproof_material',
'structure_order',
] as const;
const emptyField = requiredFields.find((field) => { const emptyField = requiredFields.find((field) => {
if (etcFields.includes(field as (typeof etcFields)[number])) { if (etcFields.includes(field as (typeof etcFields)[number])) {
return surveyDetail[field as keyof SurveyDetailRequest] === null && return surveyDetail[field as keyof SurveyDetailRequest] === null && surveyDetail[`${field}_etc` as keyof SurveyDetailRequest] === null
surveyDetail[`${field}_etc` as keyof SurveyDetailRequest] === null;
} }
return surveyDetail[field as keyof SurveyDetailRequest] === null; return surveyDetail[field as keyof SurveyDetailRequest] === null
}); })
return emptyField || ''; const contractCapacity = surveyDetail.contract_capacity
}; if (contractCapacity && contractCapacity.trim() !== '' && contractCapacity.split(' ')?.length === 1) {
return 'contract_capacity_unit'
}
return emptyField || ''
}
return { return {
surveyList: surveyList || [], surveyList: surveyList || [],

View File

@ -1,27 +1,85 @@
import { create } from 'zustand' import { create } from 'zustand'
export type SEARCH_OPTIONS_ENUM = 'all' | 'id' | 'building_name' | 'representative' | 'store' | 'construction_point'
// | 'store_id' | 'construction_id'
export type SEARCH_OPTIONS_PARTNERS_ENUM = 'all' | 'id' | 'building_name' | 'representative'
export type SORT_OPTIONS_ENUM = 'created' | 'updated'
export const SEARCH_OPTIONS = [
{
id: 'all',
label: '全体',
},
{
id: 'id',
label: '登録番号',
},
{
id: 'building_name',
label: '建物名',
},
{
id: 'representative',
label: '作成者',
},
{
id: 'store',
label: '販売店名',
},
// {
// id: 'store_id',
// label: '販売店ID',
// },
{
id: 'construction_point',
label: '施工店名',
},
// {
// id: 'construction_id',
// label: '施工店ID',
// },
]
export const SEARCH_OPTIONS_PARTNERS = [
{
id: 'all',
label: '全体',
},
{
id: 'id',
label: '登録番号',
},
{
id: 'building_name',
label: '建物名',
},
{
id: 'representative',
label: '作成者',
},
]
type SurveyFilterState = { type SurveyFilterState = {
keyword: string; keyword: string
searchOption: string; searchOption: SEARCH_OPTIONS_ENUM | SEARCH_OPTIONS_PARTNERS_ENUM
isMySurvey: boolean; isMySurvey: string | null
sort: 'recent' | 'updated'; sort: SORT_OPTIONS_ENUM
setKeyword: (keyword: string) => void; setKeyword: (keyword: string) => void
setSearchOption: (searchOption: string) => void; setSearchOption: (searchOption: SEARCH_OPTIONS_ENUM | SEARCH_OPTIONS_PARTNERS_ENUM) => void
setIsMySurvey: (isMySurvey: boolean) => void; setIsMySurvey: (isMySurvey: string | null) => void
setSort: (sort: 'recent' | 'updated') => void; setSort: (sort: SORT_OPTIONS_ENUM) => void
reset: () => void; reset: () => void
} }
export const useSurveyFilterStore = create<SurveyFilterState>((set) => ({ export const useSurveyFilterStore = create<SurveyFilterState>((set) => ({
keyword: '', keyword: '',
searchOption: '', searchOption: 'all',
isMySurvey: false, isMySurvey: null,
sort: 'recent', sort: 'created',
setKeyword: (keyword: string) => set({ keyword }), setKeyword: (keyword: string) => set({ keyword }),
setSearchOption: (searchOption: string) => set({ searchOption }), setSearchOption: (searchOption: SEARCH_OPTIONS_ENUM | SEARCH_OPTIONS_PARTNERS_ENUM) => set({ searchOption }),
setIsMySurvey: (isMySurvey: boolean) => set({ isMySurvey }), setIsMySurvey: (isMySurvey: string | null) => set({ isMySurvey }),
setSort: (sort: 'recent' | 'updated') => set({ sort }), setSort: (sort: SORT_OPTIONS_ENUM) => set({ sort }),
reset: () => set({ keyword: '', searchOption: '', isMySurvey: false, sort: 'recent' }), reset: () => set({ keyword: '', searchOption: 'all', isMySurvey: null, sort: 'created' }),
})) }))