Compare commits
4 Commits
65a30310a5
...
08d99fb4e7
| Author | SHA1 | Date | |
|---|---|---|---|
| 08d99fb4e7 | |||
| 37a40989dd | |||
| ab1e89f5d6 | |||
| a684a3e5be |
@ -14,6 +14,7 @@
|
|||||||
"@tanstack/react-query-devtools": "^5.71.0",
|
"@tanstack/react-query-devtools": "^5.71.0",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"iron-session": "^8.0.4",
|
"iron-session": "^8.0.4",
|
||||||
|
"lucide": "^0.503.0",
|
||||||
"mssql": "^11.0.1",
|
"mssql": "^11.0.1",
|
||||||
"next": "15.2.4",
|
"next": "15.2.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
|||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@ -23,6 +23,9 @@ importers:
|
|||||||
iron-session:
|
iron-session:
|
||||||
specifier: ^8.0.4
|
specifier: ^8.0.4
|
||||||
version: 8.0.4
|
version: 8.0.4
|
||||||
|
lucide:
|
||||||
|
specifier: ^0.503.0
|
||||||
|
version: 0.503.0
|
||||||
mssql:
|
mssql:
|
||||||
specifier: ^11.0.1
|
specifier: ^11.0.1
|
||||||
version: 11.0.1
|
version: 11.0.1
|
||||||
@ -1100,6 +1103,9 @@ packages:
|
|||||||
lodash.once@4.1.1:
|
lodash.once@4.1.1:
|
||||||
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||||
|
|
||||||
|
lucide@0.503.0:
|
||||||
|
resolution: {integrity: sha512-ZAVlxBU4dbSUAVidb2eT0fH3bTtKCj7M2aZNAVsFOrcnazvYJFu6I8OxFE+Fmx5XNf22Cw4Ln3NBHfBxNfoFOw==}
|
||||||
|
|
||||||
math-intrinsics@1.1.0:
|
math-intrinsics@1.1.0:
|
||||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -2319,6 +2325,8 @@ snapshots:
|
|||||||
|
|
||||||
lodash.once@4.1.1: {}
|
lodash.once@4.1.1: {}
|
||||||
|
|
||||||
|
lucide@0.503.0: {}
|
||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
micromatch@4.0.8:
|
micromatch@4.0.8:
|
||||||
|
|||||||
66
src/api/survey.ts
Normal file
66
src/api/survey.ts
Normal 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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
28
src/app/api/survey/[id]/route.ts
Normal file
28
src/app/api/survey/[id]/route.ts
Normal 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' })
|
||||||
|
}
|
||||||
16
src/app/api/survey/route.ts
Normal file
16
src/app/api/survey/route.ts
Normal 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)
|
||||||
|
}
|
||||||
9
src/app/inquiry/[id]/page.tsx
Normal file
9
src/app/inquiry/[id]/page.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import InquiryDetail from '@/components/inquiry/InquiryDetail'
|
||||||
|
|
||||||
|
export default function InquiryDetails() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<InquiryDetail />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
9
src/app/inquiry/page.tsx
Normal file
9
src/app/inquiry/page.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import InquiryList from '@/components/inquiry/InquiryList'
|
||||||
|
|
||||||
|
export default function Inquiry() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<InquiryList />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
9
src/app/inquiry/write/page.tsx
Normal file
9
src/app/inquiry/write/page.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import InquiryWriteForm from '@/components/inquiry/InquiryWriteForm'
|
||||||
|
|
||||||
|
export default function InquiryWrite() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<InquiryWriteForm />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
9
src/app/survey/[id]/page.tsx
Normal file
9
src/app/survey/[id]/page.tsx
Normal 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
10
src/app/survey/page.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import SurveySaleList from '@/components/survey/SurveySaleList'
|
||||||
|
|
||||||
|
export default function SurveyPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SurveySaleList />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
9
src/app/survey/write/page.tsx
Normal file
9
src/app/survey/write/page.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import SurveySaleWriteForm from '@/components/survey/SurveySaleWriteForm'
|
||||||
|
|
||||||
|
export default function SurveyWritePage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SurveySaleWriteForm />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
11
src/components/LoadMoreButton.tsx
Normal file
11
src/components/LoadMoreButton.tsx
Normal 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>
|
||||||
|
}
|
||||||
73
src/components/inquiry/InquiryDetail.tsx
Normal file
73
src/components/inquiry/InquiryDetail.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useParams } from 'next/navigation'
|
||||||
|
|
||||||
|
const inquiryDummyData = {
|
||||||
|
writer: {
|
||||||
|
name: 'writer',
|
||||||
|
email: 'writer@example.com',
|
||||||
|
},
|
||||||
|
title: 'title',
|
||||||
|
content: 'content',
|
||||||
|
files: ['file1.jpg', 'file2.jpg', 'file3.jpg'],
|
||||||
|
createdAt: '2021-01-01',
|
||||||
|
answer: {
|
||||||
|
writer: '佐藤一貴',
|
||||||
|
content:
|
||||||
|
'一次側接続は、自動切替開閉器と住宅分電盤主幹ブレーカの間に蓄電システムブレーカを配線する方法です。\n二次側接続は、住宅分電盤主幹ブレ―カの二次側に蓄電システムブレーカを接続する',
|
||||||
|
createdAt: '2021-01-01 12:00:00',
|
||||||
|
files: ['file4.jpg', 'file5.jpg', 'file6.jpg'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function InquiryDetail() {
|
||||||
|
const params = useParams()
|
||||||
|
const id = params.id
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>InquiryDetail</h1>
|
||||||
|
<p>{id}</p>
|
||||||
|
<div className="mt-5">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<p>writer</p>
|
||||||
|
<p>{inquiryDummyData.writer.name}</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<p>email</p>
|
||||||
|
<p>{inquiryDummyData.writer.email}</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<p>title</p>
|
||||||
|
<p>{inquiryDummyData.title}</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<p>content</p>
|
||||||
|
<p>{inquiryDummyData.content}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>files</p>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{inquiryDummyData.files.map((file) => (
|
||||||
|
<span key={file}>{file}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{inquiryDummyData.answer && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<h1>Reply: Hanwha Japan</h1>
|
||||||
|
<div>
|
||||||
|
<p>{inquiryDummyData.answer.writer}</p>
|
||||||
|
<p>{inquiryDummyData.answer.createdAt}</p>
|
||||||
|
<p>{inquiryDummyData.answer.content}</p>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{inquiryDummyData.answer.files.map((file) => (
|
||||||
|
<span key={file}>{file}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
20
src/components/inquiry/InquiryFilter.tsx
Normal file
20
src/components/inquiry/InquiryFilter.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Search } from 'lucide-react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
|
|
||||||
|
export default function InquiryFilter({ handleSearch }: { handleSearch: (e: React.ChangeEvent<HTMLInputElement>) => void }) {
|
||||||
|
const router = useRouter()
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={() => router.push('/inquiry/write')}>write 1:1 Inquiry {'>'}</button>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input type="text" placeholder="Search" onChange={handleSearch} />
|
||||||
|
<button>
|
||||||
|
<Search />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
21
src/components/inquiry/InquiryItems.tsx
Normal file
21
src/components/inquiry/InquiryItems.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
|
export default function InquiryItems({ inquiryData }: { inquiryData: any }) {
|
||||||
|
const router = useRouter()
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{inquiryData.map((item: any) => (
|
||||||
|
<div key={item.id} onClick={() => router.push(`/inquiry/${item.id}`)}>
|
||||||
|
<div>{item.title}</div>
|
||||||
|
<div>{item.content}</div>
|
||||||
|
<div>{item.createdAt}</div>
|
||||||
|
<div>{item.writer}</div>
|
||||||
|
<div>{item.category}</div>
|
||||||
|
{item.file && <div>{item.file}</div>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
171
src/components/inquiry/InquiryList.tsx
Normal file
171
src/components/inquiry/InquiryList.tsx
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
'use client'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import InquiryItems from './InquiryItems'
|
||||||
|
import InquiryFilter from './InquiryFilter'
|
||||||
|
import LoadMoreButton from '../LoadMoreButton'
|
||||||
|
|
||||||
|
const inquiryDummyData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: 'file.png',
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer',
|
||||||
|
category: 'A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: 'file.png',
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer1',
|
||||||
|
category: 'B',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: null,
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer1',
|
||||||
|
category: 'C',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: null,
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer1',
|
||||||
|
category: 'A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: null,
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer1',
|
||||||
|
category: 'B',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: null,
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer1',
|
||||||
|
category: 'C',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: 'file.png',
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer',
|
||||||
|
category: 'A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: 'file.png',
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer1',
|
||||||
|
category: 'B',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: null,
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer1',
|
||||||
|
category: 'C',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: 'file.png',
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer1',
|
||||||
|
category: 'A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: 'file.png',
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer',
|
||||||
|
category: 'B',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: null,
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer1',
|
||||||
|
category: 'C',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function InquiryList() {
|
||||||
|
const [visibleItems, setVisibleItems] = useState(5)
|
||||||
|
const [isMyPostsOnly, setIsMyPostsOnly] = useState(false)
|
||||||
|
const [category, setCategory] = useState('')
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [hasMore, setHasMore] = useState(inquiryDummyData.length > 5)
|
||||||
|
|
||||||
|
const inquriyData = () => {
|
||||||
|
if (isMyPostsOnly) {
|
||||||
|
return inquiryDummyData.filter((item) => item.writer === 'writer')
|
||||||
|
}
|
||||||
|
if (category.trim().length > 0) {
|
||||||
|
return inquiryDummyData.filter((item) => item.category === category)
|
||||||
|
}
|
||||||
|
if (search.trim().length > 0) {
|
||||||
|
return inquiryDummyData.filter((item) => item.title.includes(search))
|
||||||
|
}
|
||||||
|
return inquiryDummyData
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLoadMore = () => {
|
||||||
|
const newVisibleItems = Math.min(visibleItems + 5, inquriyData().length)
|
||||||
|
setVisibleItems(newVisibleItems)
|
||||||
|
setHasMore(newVisibleItems < inquriyData().length)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearch(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleScrollToTop = () => {
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<select onChange={(e) => setCategory(e.target.value)}>
|
||||||
|
<option value="">All</option>
|
||||||
|
<option value="A">A</option>
|
||||||
|
<option value="B">B</option>
|
||||||
|
<option value="C">C</option>
|
||||||
|
</select>
|
||||||
|
<span>total {inquriyData().length}</span>
|
||||||
|
<InquiryItems inquiryData={inquriyData().slice(0, visibleItems)} />
|
||||||
|
<LoadMoreButton hasMore={hasMore} onLoadMore={handleLoadMore} onScrollToTop={handleScrollToTop} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
72
src/components/inquiry/InquiryWriteForm.tsx
Normal file
72
src/components/inquiry/InquiryWriteForm.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
|
export interface InquiryFormData {
|
||||||
|
category: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
file: File[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function InquiryWriteForm() {
|
||||||
|
const router = useRouter()
|
||||||
|
const [formData, setFormData] = useState<InquiryFormData>({
|
||||||
|
category: 'A',
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
file: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = Array.from(e.target.files || [])
|
||||||
|
setFormData({ ...formData, file: [...formData.file, ...file] })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileDelete = (fileToDelete: File) => {
|
||||||
|
setFormData({ ...formData, file: formData.file.filter((f) => f !== fileToDelete) })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
console.log('submit: ', formData)
|
||||||
|
// router.push(`/inquiry`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="category">category</label>
|
||||||
|
<select id="category" onChange={(e) => setFormData({ ...formData, category: e.target.value })}>
|
||||||
|
<option value="A">A</option>
|
||||||
|
<option value="B">B</option>
|
||||||
|
<option value="C">C</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="title">title</label>
|
||||||
|
<input type="text" id="title" onChange={(e) => setFormData({ ...formData, title: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="content">content</label>
|
||||||
|
<textarea id="content" onChange={(e) => setFormData({ ...formData, content: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="file">file</label>
|
||||||
|
<input type="file" id="file" accept="image/*" capture="environment" onChange={handleFileChange} />
|
||||||
|
<div>
|
||||||
|
<p>file count: {formData.file.length}</p>
|
||||||
|
{formData.file.map((f) => (
|
||||||
|
<div key={f.name}>
|
||||||
|
<div>{f.name}</div>
|
||||||
|
<div>
|
||||||
|
<button onClick={() => handleFileDelete(f)}>delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={handleSubmit}>submit</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
20
src/components/survey/SurveyFilter.tsx
Normal file
20
src/components/survey/SurveyFilter.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
4
src/components/survey/SurveySaleDetail.tsx
Normal file
4
src/components/survey/SurveySaleDetail.tsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export default function SurveySaleDetail() {
|
||||||
|
return <div>SurveySaleDetail</div>
|
||||||
|
}
|
||||||
|
|
||||||
54
src/components/survey/SurveySaleList.tsx
Normal file
54
src/components/survey/SurveySaleList.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
97
src/components/survey/SurveySaleWriteForm.tsx
Normal file
97
src/components/survey/SurveySaleWriteForm.tsx
Normal 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
51
src/hooks/useSurvey.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user