feat: implement inquiry create, read function with Qcast API

This commit is contained in:
Dayoung 2025-05-13 14:13:31 +09:00
parent 8ad1ed4bcf
commit 32a8ec72ef
12 changed files with 259 additions and 102 deletions

View File

@ -7,4 +7,6 @@ NEXT_PUBLIC_API_URL=http://localhost:3000
NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120
#1:1문의 api
NEXT_PUBLIC_INQUIRY_API_URL=http://1.248.227.176:38080
NEXT_PUBLIC_INQUIRY_API_URL=http://172.23.4.129:8110

View File

@ -5,4 +5,5 @@ NEXT_PUBLIC_API_URL=http://172.30.1.35:3000
NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120
#1:1문의 api
NEXT_PUBLIC_INQUIRY_API_URL=http://1.248.227.176:38080
# NEXT_PUBLIC_INQUIRY_API_URL=http://1.248.227.176:38080
NEXT_PUBLIC_INQUIRY_API_URL=http://172.23.4.129:8110

View File

@ -2,14 +2,23 @@ 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)
const { searchParams } = new URL(request.url)
const params = {
compCd: searchParams.get('compCd'),
qnoNo: searchParams.get('qnoNo'),
langCd: searchParams.get('langCd'),
loginId: searchParams.get('loginId'),
}
try {
const response = await axios.get(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/qna/detail?${queryStringFormatter(params)}`)
if (response.status === 200) {
return NextResponse.json(response.data)
}
return NextResponse.json({ error: response.data.result }, { status: response.status })
} catch (error: any) {
console.error(error.response)
return NextResponse.json({ error: 'route error' }, { status: 500 })
}
return NextResponse.json({ error: 'Failed to fetch qna detail' }, { status: response.status })
}

View File

@ -1,17 +1,30 @@
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)}`)
const params: Record<string, string> = {}
searchParams.forEach((value, key) => {
const match = key.match(/inquiryListRequest\[(.*)\]/)
if (match) {
params[match[1]] = value
} else {
params[key] = value
}
})
if (response.status === 200) {
return NextResponse.json(response.data)
try {
const response = await axios.get(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/qna/list?${queryStringFormatter(params)}`)
if (response.status === 200) {
return NextResponse.json(response.data)
}
return NextResponse.json({ error: 'Failed to fetch qna list' }, { status: response.status })
} catch (error: any) {
console.error('Error fetching qna list:', error.response.data)
return NextResponse.json({ error: 'route error' }, { status: 500 })
}
return NextResponse.json({ error: 'Failed to fetch qna list' }, { status: response.status })
}

View File

@ -1,11 +1,10 @@
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)
const response = await axios.post(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/qna/save`, body)
if (response.status === 200) {
return NextResponse.json(response.data)

View File

@ -2,7 +2,7 @@
import { Inquiry } from '@/types/Inquiry'
export default function Answer({ inquiryDetail }: { inquiryDetail: Inquiry }) {
export default function Answer({ inquiryDetail }: { inquiryDetail: Inquiry}) {
return (
<>
<div className="inquiry-answer-wrap">

View File

@ -1,15 +1,18 @@
'use client'
import { useState } from 'react'
import Answer from './Answer'
import { useInquiry } from '@/hooks/useInquiry'
import { useRouter } from 'next/navigation'
import { useParams, useRouter } from 'next/navigation'
export default function Detail() {
//todo: 답변 완료 표시를 위해 임시로 추가 해 놓은 state
// 추후에 api 작업 완료후 삭제
// 답변 완료 클래스 & 하단 답변내용 출력도
const { inquiryDetail } = useInquiry()
const params = useParams()
const id = params.id
const { inquiryDetail } = useInquiry(Number(id), '5200')
const router = useRouter()
return (
@ -17,7 +20,9 @@ export default function Detail() {
<div className="inquiry-frame">
<div className="inquiry-detail-wrap">
<div className="inquiry-detail-badge">
<div className={`badge ${inquiryDetail?.answerYn === 'Y' ? 'orange' : 'blue'} block`}></div>
<div className={`badge ${inquiryDetail?.answerYn === 'Y' ? 'orange' : 'blue'} block`}>
{inquiryDetail?.answerYn === 'Y' ? '回答完了' : '回答待ち'}
</div>
</div>
<div className="inquiry-detail-data-table">
<table className="sale-data-table">
@ -50,9 +55,11 @@ export default function Detail() {
</table>
</div>
<div className="inquiry-detail-data">
<div className="inquiry-detail-category"></div>
<div className="inquiry-detail-category">
{inquiryDetail?.qnaClsLrgCd} - {inquiryDetail?.qnaClsMidCd}
</div>
<div className="inquiry-detail-tit">{inquiryDetail?.qstTitle}</div>
<div className="inquiry-detail-txt">{inquiryDetail?.qstContent}</div>
<div className="inquiry-detail-txt">{inquiryDetail?.qstContents}</div>
</div>
<div className="file-list-wrap">
<div className="file-list-tit"></div>
@ -68,7 +75,7 @@ export default function Detail() {
</div>
</div>
{inquiryDetail?.answerYn === 'Y' && <Answer inquiryDetail={inquiryDetail} />}
{inquiryDetail?.answerYn === 'Y' && inquiryDetail && <Answer inquiryDetail={inquiryDetail} />}
<div className="sale-edit-btn">
<button className="btn-frame n-blue icon" onClick={() => router.push('/inquiry/list')}>

View File

@ -1,5 +1,58 @@
'use client'
import { useInquiry } from '@/hooks/useInquiry'
import { useSessionStore } from '@/store/session'
import { InquiryRequest } from '@/types/Inquiry'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
export default function RegistForm() {
const { saveInquiry, isSavingInquiry } = useInquiry()
const { session } = useSessionStore()
const router = useRouter()
/// TODO: 세션 정보 적용
// useEffect(() => {
// setInquiryRequest({ ...inquiryRequest, regId: session?.userId ?? '', regUserNm: session?.userNm ?? '' })
// }, [session])
const [inquiryRequest, setInquiryRequest] = useState<InquiryRequest>({
compCd: '5200',
siteTpCd: 'QC',
qnaClsLrgCd: '',
qnaClsMidCd: 'B02',
qnaClsSmlCd: null,
title: '',
contents: null,
regId: '',
regUserNm: 'TEST',
regUserTelNo: null,
storeId: null,
})
const [attachedFiles, setAttachedFiles] = useState<File[]>([])
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
if (files && files.length > 0) {
setAttachedFiles(attachedFiles.concat(Array.from(files)))
}
e.target.value = ''
}
const handleRemoveFile = (index: number) => {
setAttachedFiles(attachedFiles.filter((_, i) => i !== index))
}
const handleSubmit = async () => {
if (confirm('お問い合わせを登録しますか? Hanwha Japanの担当者にお問い合わせメールが送信されます。')) {
const res = await saveInquiry(inquiryRequest)
alert('保存されました。')
router.push(`/inquiry/${res.qnaNo}`)
}
return
}
return (
<>
<div className="inquiry-frame">
@ -9,12 +62,21 @@ export default function RegistForm() {
<i className="import">*</i>
</div>
<div className="data-input">
<select className="select-form" name="" id="">
<option value=""></option>
<option value=""></option>
<option value=""></option>
<option value=""></option>
<option value=""></option>
<select
className="select-form"
name="qnaClsLrgCd"
id="qnaClsLrgCd"
defaultValue=""
onChange={(e) => setInquiryRequest({ ...inquiryRequest, qnaClsLrgCd: e.target.value })}
>
<option value="" hidden>
</option>
<option value="A01">A01</option>
<option value="A02">A02</option>
<option value="A03">A03</option>
<option value="A04">A04</option>
<option value="A05">A05</option>
</select>
</div>
</div>
@ -23,12 +85,19 @@ export default function RegistForm() {
<i className="import">*</i>
</div>
<div className="data-input">
<select className="select-form" name="" id="">
<option value=""></option>
<option value=""></option>
<option value=""></option>
<option value=""></option>
<option value=""></option>
<select
className="select-form"
name="title"
id="title"
onChange={(e) => setInquiryRequest({ ...inquiryRequest, title: e.target.value })}
>
<option value="" hidden>
</option>
<option value="TEST">TEST</option>
<option value="TEST2">TEST2</option>
<option value="TEST3">TEST3</option>
<option value="TEST4">TEST4</option>
</select>
</div>
</div>
@ -37,7 +106,14 @@ export default function RegistForm() {
<i className="import">*</i>
</div>
<div className="data-input">
<textarea className="textarea-form" rows={6} name="" id="" placeholder="TextArea Filed"></textarea>
<textarea
className="textarea-form"
rows={6}
name=""
id=""
placeholder="TextArea Filed"
onChange={(e) => setInquiryRequest({ ...inquiryRequest, contents: e.target.value })}
></textarea>
</div>
</div>
</div>
@ -46,29 +122,25 @@ export default function RegistForm() {
<label className="btn-frame l-blue icon" htmlFor="file">
<i className="btn-clip"></i>Attach
</label>
<input type="file" id="file" />
<input type="file" id="file" onChange={handleFileChange} multiple style={{ display: 'none' }} />
</div>
<div className="file-list-wrap">
<div className="file-list-tit">
<span>2</span>
<span>{attachedFiles.length}</span>
</div>
<ul className="file-list">
<li className="file-item">
<div className="file-item-bx">
<div className="file-item-name">.jpg </div>
<button className="file-del"></button>
</div>
</li>
<li className="file-item">
<div className="file-item-bx">
<div className="file-item-name">.jpg </div>
<button className="file-del"></button>
</div>
</li>
{attachedFiles.map((file, index) => (
<li className="file-item" key={`${file.name}-${index}`}>
<div className="file-item-bx">
<div className="file-item-name">{file.name}</div>
<button className="file-del" onClick={() => handleRemoveFile(index)} aria-label="Remove file" />
</div>
</li>
))}
</ul>
</div>
<div className="sale-edit-btn">
<button className="btn-frame n-blue icon">
<button className="btn-frame n-blue icon" onClick={handleSubmit} disabled={isSavingInquiry}>
<i className="btn-arr"></i>
</button>
</div>

View File

@ -1,8 +1,25 @@
'use client'
import { useInquiryFilterStore } from '@/store/inquiryFilterStore'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
export default function ListForm() {
const router = useRouter()
const [searchKeyword, setSearchKeyword] = useState('')
const { inquiryListRequest, setInquiryListRequest } = useInquiryFilterStore()
const handleSearch = () => {
if (searchKeyword.length >= 2) {
setInquiryListRequest({ ...inquiryListRequest, schTitle: searchKeyword })
}
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleSearch()
}
}
return (
<>
<div className="sale-frame">
@ -13,8 +30,15 @@ export default function ListForm() {
</div>
<div className="sale-form-bx">
<div className="search-input">
<input type="text" className="search-frame" placeholder="タイトルを入力してください. (2文字以上)" />
<button className="search-icon"></button>
<input
type="text"
className="search-frame"
placeholder="タイトルを入力してください. (2文字以上)"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onKeyDown={handleKeyDown}
/>
<button className="search-icon" onClick={handleSearch}></button>
</div>
</div>
</div>

View File

@ -4,6 +4,9 @@ import { useEffect, useState } from 'react'
import LoadMoreButton from '../../LoadMoreButton'
import { useInquiry } from '@/hooks/useInquiry'
import { InquiryList } from '@/types/Inquiry'
import { useRouter } from 'next/navigation'
import { useInquiryFilterStore } from '@/store/inquiryFilterStore'
import { useSessionStore } from '@/store/session'
const badgeStyle = [
{
@ -21,14 +24,30 @@ export default function ListTable() {
const [offset, setOffset] = useState(0)
const [hasMore, setHasMore] = useState(true)
const router = useRouter()
const { inquiryList } = useInquiry()
const { inquiryListRequest, setInquiryListRequest } = useInquiryFilterStore()
const { session } = useSessionStore()
useEffect(() => {
if (inquiryList.length > offset + 10) {
setHasMore(true)
if (inquiryList.length !== 0) {
const hasMoreItems = inquiryList[0].totCnt > offset + 10
setHasMore(hasMoreItems)
} else {
setHasMore(false)
}
}, [inquiryList])
}, [inquiryList, offset])
const handleMyInquiry = () => {
setInquiryListRequest({ ...inquiryListRequest, schRegId: inquiryListRequest.schRegId ? null : session.userId })
}
const handleLoadMore = () => {
setOffset(offset + 10)
setInquiryListRequest({ ...inquiryListRequest, startRow: offset, endRow: offset + 10 })
}
return (
<>
@ -36,7 +55,7 @@ export default function ListTable() {
<div className="inquiry-table-filter">
<div className="filter-check">
<div className="check-form-box">
<input type="checkbox" id="ch01" />
<input type="checkbox" id="ch01" onChange={handleMyInquiry} />
<label htmlFor="ch01"></label>
</div>
</div>
@ -53,21 +72,22 @@ export default function ListTable() {
<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}
{inquiryList.length > 0 &&
inquiryList.map((inquiry: InquiryList) => (
<li className="inquiry-item" key={inquiry.qnaNo} onClick={() => router.push(`/inquiry/${inquiry.qnaNo}`)}>
<div className="inquiry-item-bx">
<div className="inquiry-item-category">{inquiry.qnaClsLrgCd}</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>
</div>
</li>
))}
</li>
))}
</ul>
<div className="sale-edit-btn">
<LoadMoreButton hasMore={hasMore} onLoadMore={() => setOffset(offset + 10)} />
<LoadMoreButton hasMore={hasMore} onLoadMore={() => handleLoadMore()} />
</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
import { InquiryList, Inquiry, InquiryRequest } from '@/types/Inquiry'
import { InquiryList, Inquiry, InquiryRequest, InquirySaveResponse } from '@/types/Inquiry'
import { axiosInstance } from '@/libs/axios'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useInquiryFilterStore } from '@/store/inquiryFilterStore'
@ -12,37 +12,47 @@ export function useInquiry(
inquiryDetail: Inquiry | null
isLoadingInquiryDetail: boolean
isSavingInquiry: boolean
saveInquiry: (inquiryRequest: InquiryRequest) => Promise<Inquiry>
saveInquiry: (inquiryRequest: InquiryRequest) => Promise<InquirySaveResponse>
} {
const { session } = useSessionStore()
const queryClient = useQueryClient()
const { inquiryListRequest } = useInquiryFilterStore()
const { data: inquiryList, isLoading: isLoadingInquiryList } = useQuery({
queryKey: ['inquiryList', qnoNo, compCd, inquiryListRequest],
queryKey: ['inquiryList', inquiryListRequest],
queryFn: async () => {
const resp = await axiosInstance(null).get<InquiryList[]>('/api/qna/list', {
params: { inquiryListRequest },
})
return resp.data
try {
const resp = await axiosInstance(null).get<{ data: InquiryList[] }>(`/api/qna/list`, {
params: { inquiryListRequest },
})
return resp.data.data
} catch (error: any) {
console.error(error.response.data)
return []
}
},
})
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
try {
const resp = await axiosInstance(null).get<{ data: Inquiry }>(`/api/qna/detail`, {
params: { qnoNo, compCd, langCd: 'JA', loginId: 'x112' },
})
return resp.data.data
} catch (error: any) {
console.error(error.response)
return null
}
},
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
const resp = await axiosInstance(null).post<{ data: InquirySaveResponse }>('/api/qna/save', inquiryRequest)
return resp.data.data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['inquiryList'] })

View File

@ -9,33 +9,33 @@ type InquiryFilterState = {
export const useInquiryFilterStore = create<InquiryFilterState>((set) => ({
inquiryListRequest: {
compCd: '',
langCd: '',
storeId: '',
siteTpCd: '',
schTitle: '',
schRegId: '',
schFromDt: '',
schToDt: '',
compCd: '5200',
langCd: 'JA',
storeId: 'X112',
siteTpCd: 'QC',
schTitle: null,
schRegId: null,
schFromDt: null,
schToDt: null,
startRow: 0,
endRow: 0,
loginId: '',
endRow: 10,
loginId: 'x112',
},
setInquiryListRequest: (inquiryListRequest) => set({ inquiryListRequest }),
reset: () =>
set({
inquiryListRequest: {
compCd: '',
langCd: '',
storeId: '',
siteTpCd: '',
compCd: '5200',
langCd: 'JA',
storeId: 'X112',
siteTpCd: 'QC',
schTitle: '',
schRegId: '',
schFromDt: '',
schToDt: '',
startRow: 0,
endRow: 0,
loginId: '',
endRow: 50,
loginId: 'x112',
},
}),
}))