feat: implement Qsp Inquriy API Request, Response type
This commit is contained in:
parent
34319dadcd
commit
e3940f72c8
15
src/app/api/qna/detail/route.ts
Normal file
15
src/app/api/qna/detail/route.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { queryStringFormatter } from '@/utils/common-utils'
|
||||
import axios from 'axios'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export const QSP_URL = 'http://localhost:8080'
|
||||
export async function GET(request: Request) {
|
||||
const body = await request.json()
|
||||
|
||||
const response = await axios.get(`${QSP_URL}/qna/detail?${queryStringFormatter(body)}`)
|
||||
|
||||
if (response.status === 200) {
|
||||
return NextResponse.json(response.data)
|
||||
}
|
||||
return NextResponse.json({ error: 'Failed to fetch qna detail' }, { status: response.status })
|
||||
}
|
||||
17
src/app/api/qna/list/route.ts
Normal file
17
src/app/api/qna/list/route.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import axios from 'axios'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { queryStringFormatter } from '@/utils/common-utils'
|
||||
import { QSP_URL } from '../detail/route'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
console.log('searchParams::: ', searchParams)
|
||||
|
||||
const response = await axios.get(`${QSP_URL}/qna/list?${queryStringFormatter(searchParams)}`)
|
||||
|
||||
if (response.status === 200) {
|
||||
return NextResponse.json(response.data)
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Failed to fetch qna list' }, { status: response.status })
|
||||
}
|
||||
15
src/app/api/qna/save/route.ts
Normal file
15
src/app/api/qna/save/route.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import axios from 'axios'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { QSP_URL } from '../detail/route'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json()
|
||||
|
||||
const response = await axios.post(`${QSP_URL}/qna/save`, body)
|
||||
|
||||
if (response.status === 200) {
|
||||
return NextResponse.json(response.data)
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Failed to save qna' }, { status: response.status })
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import ListForm from '@/components/inquiry/ListForm'
|
||||
import ListTable from '@/components/inquiry/ListTable'
|
||||
import ListForm from '@/components/inquiry/list/ListForm'
|
||||
import ListTable from '@/components/inquiry/list/ListTable'
|
||||
|
||||
export default function page() {
|
||||
return (
|
||||
|
||||
@ -1,35 +1,31 @@
|
||||
'use client'
|
||||
|
||||
export default function Answer() {
|
||||
import { Inquiry } from '@/types/Inquiry'
|
||||
|
||||
export default function Answer({ inquiryDetail }: { inquiryDetail: Inquiry }) {
|
||||
return (
|
||||
<>
|
||||
<div className="inquiry-answer-wrap">
|
||||
<div className="inquiry-answer-header">
|
||||
<div className="inquiry-answer-tit">Hanwha Japan 回答</div>
|
||||
<div className="inquiry-answer-date">
|
||||
<span>佐藤一貴</span>/ <span>2025.04.02 16:54:00</span>
|
||||
<span>{inquiryDetail?.ansRegNm}</span>/ <span>{inquiryDetail?.ansRegDt}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="inquiry-detail-data">
|
||||
<div className="inquiry-detail-category">回答</div>
|
||||
<div className="inquiry-detail-txt">
|
||||
一次側接続は, 自動切替開閉器と住宅分電盤昼間遮断器との間に蓄電システム遮断器を配線する方法です. 二次側接続は,
|
||||
住宅分電盤週間ブレーカの二次側に蓄電システムブレーカを接続する
|
||||
</div>
|
||||
<div className="inquiry-detail-txt">{inquiryDetail?.ansContents}</div>
|
||||
</div>
|
||||
<div className="file-list-wrap">
|
||||
<div className="file-list-tit">ファイル添付</div>
|
||||
<ul className="file-list">
|
||||
<li className="file-item">
|
||||
{inquiryDetail?.ansListFile?.map((file) => (
|
||||
<li className="file-item" key={file.fileNo}>
|
||||
<button className="file-item-bx">
|
||||
<div className="file-item-name">添付ファイル名.jpg </div>
|
||||
</button>
|
||||
</li>
|
||||
<li className="file-item">
|
||||
<button className="file-item-bx">
|
||||
<div className="file-item-name">添付ファイル名.jpg </div>
|
||||
<div className="file-item-name">{file.srcFileNm} </div>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,19 +2,22 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import Answer from './Answer'
|
||||
import { useInquiry } from '@/hooks/useInquiry'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
export default function Detail() {
|
||||
//todo: 답변 완료 표시를 위해 임시로 추가 해 놓은 state
|
||||
// 추후에 api 작업 완료후 삭제
|
||||
// 답변 완료 클래스 & 하단 답변내용 출력도
|
||||
const [inquiry, setInquiry] = useState<Boolean>(false)
|
||||
const { inquiryDetail } = useInquiry()
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="inquiry-frame">
|
||||
<div className="inquiry-detail-wrap">
|
||||
<div className="inquiry-detail-badge">
|
||||
<div className={`badge ${inquiry ? 'orange' : 'blue'} block`}>回答完了</div>
|
||||
<div className={`badge ${inquiryDetail?.answerYn === 'Y' ? 'orange' : 'blue'} block`}>回答完了</div>
|
||||
</div>
|
||||
<div className="inquiry-detail-data-table">
|
||||
<table className="sale-data-table">
|
||||
@ -25,59 +28,50 @@ export default function Detail() {
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>登録日</th>
|
||||
<td>2025.04.10</td>
|
||||
<td>{inquiryDetail?.regDt}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>作者</th>
|
||||
<td>Hong gi</td>
|
||||
<td>{inquiryDetail?.regNm}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>販売店</th>
|
||||
<td>interplug</td>
|
||||
<td>{inquiryDetail?.storeNm}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>施工店</th>
|
||||
<td>interplugs</td>
|
||||
<td>{inquiryDetail?.compCd}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>E-mail</th>
|
||||
<td>Hong@interplug.co.kr</td>
|
||||
<td>{inquiryDetail?.regEmail}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="inquiry-detail-data">
|
||||
<div className="inquiry-detail-category">屋根適合</div>
|
||||
<div className="inquiry-detail-tit">屋根材適合性確認依頼</div>
|
||||
<div className="inquiry-detail-txt">
|
||||
入力した内容が表示されます.
|
||||
<br />
|
||||
インストール可能であることを確認してください.
|
||||
<br />
|
||||
屋根の写真を添付しました.
|
||||
</div>
|
||||
<div className="inquiry-detail-tit">{inquiryDetail?.qstTitle}</div>
|
||||
<div className="inquiry-detail-txt">{inquiryDetail?.qstContent}</div>
|
||||
</div>
|
||||
<div className="file-list-wrap">
|
||||
<div className="file-list-tit">ファイル添付</div>
|
||||
<ul className="file-list">
|
||||
{inquiryDetail?.listFile?.map((file) => (
|
||||
<li className="file-item">
|
||||
<button className="file-item-bx">
|
||||
<div className="file-item-name">添付ファイル名.jpg </div>
|
||||
</button>
|
||||
</li>
|
||||
<li className="file-item">
|
||||
<button className="file-item-bx">
|
||||
<div className="file-item-name">添付ファイル名.jpg </div>
|
||||
<div className="file-item-name">{file.srcFileNm} </div>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{inquiry && <Answer />}
|
||||
{inquiryDetail?.answerYn === 'Y' && <Answer inquiryDetail={inquiryDetail} />}
|
||||
|
||||
<div className="sale-edit-btn">
|
||||
<button className="btn-frame n-blue icon">
|
||||
<button className="btn-frame n-blue icon" onClick={() => router.push('/inquiry/list')}>
|
||||
リスト<i className="btn-arr"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -1,73 +0,0 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
@ -1,171 +0,0 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
@ -1,67 +0,0 @@
|
||||
'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 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>delete</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={handleSubmit}>submit</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,93 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { use, useEffect, useState } from 'react'
|
||||
import LoadMoreButton from '../LoadMoreButton'
|
||||
|
||||
const inquiryDummy = [
|
||||
{ id: 1, category: '屋根', title: '屋根材適合性確認依頼', date: '2025.04.02', status: 'completed' },
|
||||
{ id: 2, category: '外壁', title: '外壁仕上げ材確認', date: '2025.04.03', status: 'completed' },
|
||||
{ id: 3, category: '設備', title: '換気システム図面確認', date: '2025.04.04', status: 'completed' },
|
||||
{ id: 4, category: '基礎', title: '基礎配筋検査依頼', date: '2025.04.05', status: 'completed' },
|
||||
{ id: 5, category: '内装', title: 'クロス仕様確認', date: '2025.04.06', status: 'waiting' },
|
||||
{ id: 6, category: '構造', title: '耐震壁位置変更申請', date: '2025.04.07', status: 'completed' },
|
||||
{ id: 7, category: '屋根', title: '雨樋取付方法確認', date: '2025.04.08', status: 'completed' },
|
||||
{ id: 8, category: '外構', title: 'フェンス高さ変更相談', date: '2025.04.09', status: 'completed' },
|
||||
{ id: 9, category: '設備', title: '給湯器設置位置確認', date: '2025.04.10', status: 'completed' },
|
||||
{ id: 10, category: '外壁', title: 'タイル割付案確認依頼', date: '2025.04.11', status: 'waiting' },
|
||||
{ id: 11, category: '内装', title: '照明配置図面確認', date: '2025.04.12', status: 'completed' },
|
||||
{ id: 12, category: '構造', title: '梁補強案確認', date: '2025.04.13', status: 'completed' },
|
||||
{ id: 13, category: '基礎', title: '杭長設計確認依頼', date: '2025.04.14', status: 'completed' },
|
||||
{ id: 14, category: '屋根', title: '断熱材施工方法確認', date: '2025.04.15', status: 'completed' },
|
||||
{ id: 15, category: '外構', title: '駐車場勾配図確認', date: '2025.04.16', status: 'completed' },
|
||||
]
|
||||
|
||||
const badgeStyle = [
|
||||
{
|
||||
id: 'completed',
|
||||
label: '回答完了',
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
id: 'waiting',
|
||||
label: '回答待ち',
|
||||
color: 'orange',
|
||||
},
|
||||
]
|
||||
export default function ListTable() {
|
||||
const [offset, setOffset] = useState(0)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
|
||||
const inquiryList = inquiryDummy.slice(0, offset + 10)
|
||||
|
||||
useEffect(() => {
|
||||
if (inquiryDummy.length > offset + 10) {
|
||||
setHasMore(true)
|
||||
} else {
|
||||
setHasMore(false)
|
||||
}
|
||||
}, [inquiryList])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sale-frame">
|
||||
<div className="inquiry-table-filter">
|
||||
<div className="filter-check">
|
||||
<div className="check-form-box">
|
||||
<input type="checkbox" id="ch01" />
|
||||
<label htmlFor="ch01">私が書いたお問い合わせ</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="filter-select">
|
||||
<select className="select-form" name="" id="">
|
||||
<option value="">全体</option>
|
||||
<option value="">回答待ち</option>
|
||||
<option value="">回答完了</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="inquiry-list-wrap">
|
||||
<div className="inquiry-list-tit">
|
||||
合計 <span>98</span>個
|
||||
</div>
|
||||
<ul className="inquiry-list">
|
||||
{inquiryList.map((inquiry) => (
|
||||
<li className="inquiry-item" key={inquiry.id}>
|
||||
<div className="inquiry-item-bx">
|
||||
<div className="inquiry-item-category">{inquiry.category}</div>
|
||||
<div className="inquiry-item-tit">{inquiry.title}</div>
|
||||
<div className="inquiry-item-date">{inquiry.date}</div>
|
||||
<div className={`inquiry-badge badge ${badgeStyle.find((badge) => badge.id === inquiry.status)?.color}`}>
|
||||
{badgeStyle.find((badge) => badge.id === inquiry.status)?.label}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="sale-edit-btn">
|
||||
<LoadMoreButton hasMore={hasMore} onLoadMore={() => setOffset(offset + 10)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
76
src/components/inquiry/list/ListTable.tsx
Normal file
76
src/components/inquiry/list/ListTable.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import LoadMoreButton from '../../LoadMoreButton'
|
||||
import { useInquiry } from '@/hooks/useInquiry'
|
||||
import { InquiryList } from '@/types/Inquiry'
|
||||
|
||||
const badgeStyle = [
|
||||
{
|
||||
id: 'Y',
|
||||
label: '回答完了',
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
id: 'N',
|
||||
label: '回答待ち',
|
||||
color: 'orange',
|
||||
},
|
||||
]
|
||||
export default function ListTable() {
|
||||
const [offset, setOffset] = useState(0)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
|
||||
const { inquiryList } = useInquiry()
|
||||
useEffect(() => {
|
||||
if (inquiryList.length > offset + 10) {
|
||||
setHasMore(true)
|
||||
} else {
|
||||
setHasMore(false)
|
||||
}
|
||||
}, [inquiryList])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sale-frame">
|
||||
<div className="inquiry-table-filter">
|
||||
<div className="filter-check">
|
||||
<div className="check-form-box">
|
||||
<input type="checkbox" id="ch01" />
|
||||
<label htmlFor="ch01">私が書いたお問い合わせ</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="filter-select">
|
||||
<select className="select-form" name="" id="">
|
||||
<option value="">全体</option>
|
||||
<option value="N">回答待ち</option>
|
||||
<option value="Y">回答完了</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="inquiry-list-wrap">
|
||||
<div className="inquiry-list-tit">
|
||||
合計 <span>{inquiryList.length}</span>個
|
||||
</div>
|
||||
<ul className="inquiry-list">
|
||||
{inquiryList.map((inquiry: InquiryList) => (
|
||||
<li className="inquiry-item" key={inquiry.qnaNo}>
|
||||
<div className="inquiry-item-bx">
|
||||
<div className="inquiry-item-category">{inquiry.qnaTpCd}</div>
|
||||
<div className="inquiry-item-tit">{inquiry.qstTitle}</div>
|
||||
<div className="inquiry-item-date">{inquiry.regDt}</div>
|
||||
<div className={`inquiry-badge badge ${badgeStyle.find((badge) => badge.id === inquiry.answerYn)?.color}`}>
|
||||
{badgeStyle.find((badge) => badge.id === inquiry.answerYn)?.label}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="sale-edit-btn">
|
||||
<LoadMoreButton hasMore={hasMore} onLoadMore={() => setOffset(offset + 10)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
60
src/hooks/useInquiry.ts
Normal file
60
src/hooks/useInquiry.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { InquiryList, Inquiry, InquiryRequest } from '@/types/Inquiry'
|
||||
import { axiosInstance } from '@/libs/axios'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useInquiryFilterStore } from '@/store/inquiryFilterStore'
|
||||
import { useSessionStore } from '@/store/session'
|
||||
export function useInquiry(
|
||||
qnoNo?: number,
|
||||
compCd?: string,
|
||||
): {
|
||||
inquiryList: InquiryList[]
|
||||
isLoadingInquiryList: boolean
|
||||
inquiryDetail: Inquiry | null
|
||||
isLoadingInquiryDetail: boolean
|
||||
isSavingInquiry: boolean
|
||||
saveInquiry: (inquiryRequest: InquiryRequest) => Promise<Inquiry>
|
||||
} {
|
||||
const { session } = useSessionStore()
|
||||
const queryClient = useQueryClient()
|
||||
const { inquiryListRequest } = useInquiryFilterStore()
|
||||
|
||||
const { data: inquiryList, isLoading: isLoadingInquiryList } = useQuery({
|
||||
queryKey: ['inquiryList', qnoNo, compCd, inquiryListRequest],
|
||||
queryFn: async () => {
|
||||
const resp = await axiosInstance(null).get<InquiryList[]>('/api/qna/list', {
|
||||
params: { inquiryListRequest },
|
||||
})
|
||||
return resp.data
|
||||
},
|
||||
})
|
||||
|
||||
const { data: inquiryDetail, isLoading: isLoadingInquiryDetail } = useQuery({
|
||||
queryKey: ['inquiryDetail', qnoNo, compCd],
|
||||
queryFn: async () => {
|
||||
const resp = await axiosInstance(null).get<Inquiry>(`/api/qna/detail`, {
|
||||
params: { qnoNo, compCd, loginId: session?.userNm },
|
||||
})
|
||||
return resp.data
|
||||
},
|
||||
enabled: qnoNo !== undefined && compCd !== undefined,
|
||||
})
|
||||
|
||||
const { mutateAsync: saveInquiry, isPending: isSavingInquiry } = useMutation({
|
||||
mutationFn: async (inquiryRequest: InquiryRequest) => {
|
||||
const resp = await axiosInstance(null).post<Inquiry>('/api/qna/save', inquiryRequest)
|
||||
return resp.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['inquiryList'] })
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
inquiryList: inquiryList ?? [],
|
||||
inquiryDetail: inquiryDetail ?? null,
|
||||
isLoadingInquiryList,
|
||||
isLoadingInquiryDetail,
|
||||
isSavingInquiry,
|
||||
saveInquiry,
|
||||
}
|
||||
}
|
||||
@ -1,41 +1,41 @@
|
||||
import { InquiryListRequest } from '@/types/Inquiry'
|
||||
import { create } from 'zustand'
|
||||
|
||||
export const FILTER_OPTIONS = [
|
||||
{
|
||||
id: 'all',
|
||||
label: '全体',
|
||||
},
|
||||
{
|
||||
id: 'completed',
|
||||
label: '回答完了',
|
||||
},
|
||||
{
|
||||
id: 'waiting',
|
||||
label: '回答待ち',
|
||||
},
|
||||
]
|
||||
export type FILTER_OPTIONS_ENUM = (typeof FILTER_OPTIONS)[number]['id']
|
||||
|
||||
type InquiryFilterState = {
|
||||
keyword: string
|
||||
filter: FILTER_OPTIONS_ENUM
|
||||
isMySurvey: string | null
|
||||
offset: number
|
||||
setKeyword: (keyword: string) => void
|
||||
setFilter: (filter: FILTER_OPTIONS_ENUM) => void
|
||||
setIsMySurvey: (isMySurvey: string | null) => void
|
||||
setOffset: (offset: number) => void
|
||||
inquiryListRequest: InquiryListRequest
|
||||
setInquiryListRequest: (inquiryListRequest: InquiryListRequest) => void
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
export const useInquiryFilterStore = create<InquiryFilterState>((set) => ({
|
||||
keyword: '',
|
||||
filter: 'all',
|
||||
isMySurvey: null,
|
||||
offset: 0,
|
||||
setKeyword: (keyword) => set({ keyword }),
|
||||
setFilter: (filter) => set({ filter }),
|
||||
setIsMySurvey: (isMySurvey) => set({ isMySurvey }),
|
||||
setOffset: (offset) => set({ offset }),
|
||||
reset: () => set({ keyword: '', filter: 'all', isMySurvey: null, offset: 0 }),
|
||||
inquiryListRequest: {
|
||||
compCd: '',
|
||||
langCd: '',
|
||||
storeId: '',
|
||||
siteTpCd: '',
|
||||
schTitle: '',
|
||||
schRegId: '',
|
||||
schFromDt: '',
|
||||
schToDt: '',
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
loginId: '',
|
||||
},
|
||||
setInquiryListRequest: (inquiryListRequest) => set({ inquiryListRequest }),
|
||||
reset: () =>
|
||||
set({
|
||||
inquiryListRequest: {
|
||||
compCd: '',
|
||||
langCd: '',
|
||||
storeId: '',
|
||||
siteTpCd: '',
|
||||
schTitle: '',
|
||||
schRegId: '',
|
||||
schFromDt: '',
|
||||
schToDt: '',
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
loginId: '',
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
89
src/types/Inquiry.ts
Normal file
89
src/types/Inquiry.ts
Normal file
@ -0,0 +1,89 @@
|
||||
export type InquiryListRequest = {
|
||||
compCd: string //company code
|
||||
langCd: string //language code
|
||||
storeId: string //store id
|
||||
siteTpCd: string //site type code (QC: QCast, QR: QRead)
|
||||
schTitle: string | null //search title
|
||||
schRegId: string | null //search regId
|
||||
schFromDt: string | null //search start date
|
||||
schToDt: string | null //search end date
|
||||
startRow: number //start row
|
||||
endRow: number //end row
|
||||
loginId: string //login id
|
||||
}
|
||||
|
||||
export type InquiryList = {
|
||||
totCnt: number //total count
|
||||
rowNumber: number //row number
|
||||
compCd: string //company code
|
||||
qnaNo: number //qna number
|
||||
qstTitle: string //title
|
||||
regDt: string //registration date
|
||||
regId: string //registration Userid
|
||||
regNm: string //registration User name
|
||||
answerYn: string //answer yn - Y / N
|
||||
attachYn: string | null //attach yn - Y / N
|
||||
qnaClsLrgCd: string //qna CLS large Code
|
||||
qnaClsMidCd: string //qna CLS Mid Code
|
||||
qnaClsSmlCd: string | null //qna CLS Small Code
|
||||
}
|
||||
|
||||
export type InquiryDetailRequest = {
|
||||
compCd: string //company code
|
||||
langCd: string //language code
|
||||
qnaNo: number //qna number
|
||||
loginId: string //login id
|
||||
}
|
||||
|
||||
export type Inquiry = {
|
||||
compCd: string //company code
|
||||
qnaNo: number //qna number
|
||||
qstTitle: string //title
|
||||
qstContents: string //content
|
||||
regDt: string //registration date
|
||||
regId: string //registration Userid
|
||||
regNm: string //registration User name
|
||||
regEmail: string //registration User email
|
||||
answerYn: string //answer yn - Y / N
|
||||
ansContents: string | null //answer content
|
||||
ansRegDt: string | null //answer registration date
|
||||
ansRegNm: string | null //answer registration User name
|
||||
storeId: string | null //store id
|
||||
storeNm: string | null //store name
|
||||
regUserNm: string //registration User name
|
||||
regUserTelNo: string | null //registration User tel number
|
||||
qnaClsLrgCd: string //qna CLS large Code
|
||||
qnaClsMidCd: string //qna CLS Mid Code
|
||||
qnaClsSmlCd: string | null //qna CLS Small Code
|
||||
listFile: listFile[] | null //Question list file
|
||||
ansListFile: listFile[] | null //Answer list file
|
||||
}
|
||||
|
||||
export type listFile = {
|
||||
fileNo: number //file number
|
||||
encodeFileNo: string //encode file number
|
||||
srcFileNm: string //source file name
|
||||
fileCours: string //file course
|
||||
fileSize: number //file size(Byte)
|
||||
regDt: string //registration date
|
||||
}
|
||||
|
||||
export type InquiryRequest = {
|
||||
compCd: string //company code
|
||||
siteTpCd: string //site type code(QC: QCast, QR: QRead)
|
||||
qnaClsLrgCd: string //qna CLS large Code
|
||||
qnaClsMidCd: string //qna CLS Mid Code
|
||||
qnaClsSmlCd: string | null //qna CLS Small Code
|
||||
title: string //title
|
||||
contents: string | null //contents
|
||||
regId: string //registration Userid
|
||||
storeId: string | null //store id
|
||||
regUserNm: string //registration User name
|
||||
regUserTelNo: string | null //registration User tel number
|
||||
}
|
||||
|
||||
export type InquirySaveResponse = {
|
||||
cnt: number | null //count
|
||||
qnaNo: number //qna number
|
||||
mailYn: string //mail yn - Y / N
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user