김장현
11 months ago
13 changed files with 585 additions and 108 deletions
@ -0,0 +1,207 @@ |
|||||||
|
import { |
||||||
|
Button, |
||||||
|
Input, |
||||||
|
Modal, |
||||||
|
ModalHeader, |
||||||
|
ModalBody, |
||||||
|
ModalFooter, |
||||||
|
Row, |
||||||
|
Col, |
||||||
|
FormGroup, |
||||||
|
Label, |
||||||
|
CustomInput |
||||||
|
} from 'reactstrap'; |
||||||
|
|
||||||
|
export default function QnaDetatil({ |
||||||
|
isDetailModal, |
||||||
|
handlerDetailModal, |
||||||
|
adminDetailForm, |
||||||
|
handlerChangeDetailForm, |
||||||
|
handlerSubmitDetailForm |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<Modal |
||||||
|
isOpen={isDetailModal} |
||||||
|
toggle={handlerDetailModal} |
||||||
|
className='modal-dialog-centered modal-xl faq-modal' |
||||||
|
> |
||||||
|
<ModalHeader toggle={handlerDetailModal}>문의내역 답변</ModalHeader> |
||||||
|
<div className='pal-form'> |
||||||
|
<ModalBody> |
||||||
|
<Row> |
||||||
|
<Col className='list-input' md='4'> |
||||||
|
<FormGroup> |
||||||
|
<Label for='test'>작성자</Label> |
||||||
|
<Input |
||||||
|
type='text' |
||||||
|
bsSize='sm' |
||||||
|
placeholder='' |
||||||
|
value={adminDetailForm.createUserNm} |
||||||
|
disabled |
||||||
|
/> |
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
<Col className='list-input' md='8'> |
||||||
|
<FormGroup> |
||||||
|
<Label for='test'> |
||||||
|
<span className='necessary'></span>제목 |
||||||
|
</Label> |
||||||
|
<Input |
||||||
|
type='text' |
||||||
|
bsSize='sm' |
||||||
|
placeholder='' |
||||||
|
disabled |
||||||
|
value={adminDetailForm.title} |
||||||
|
/> |
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
<Row> |
||||||
|
<Col className='list-input' md='4'> |
||||||
|
<FormGroup className='feedback-input'> |
||||||
|
<Label for='test'> |
||||||
|
<span className='necessary'></span>연락처 |
||||||
|
</Label> |
||||||
|
<Input |
||||||
|
type='text' |
||||||
|
bsSize='sm' |
||||||
|
placeholder='' |
||||||
|
value='010-1111-1111' |
||||||
|
disabled |
||||||
|
/> |
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
<Col className='list-input' md='4'> |
||||||
|
<FormGroup> |
||||||
|
<Label for='test'>문의 유형</Label> |
||||||
|
<Input |
||||||
|
type='text' |
||||||
|
bsSize='sm' |
||||||
|
placeholder='' |
||||||
|
value={adminDetailForm.category} |
||||||
|
disabled |
||||||
|
/> |
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
<Col className='list-input' md='4'> |
||||||
|
<FormGroup> |
||||||
|
<Label for='test'>첨부 파일</Label> |
||||||
|
<Input |
||||||
|
type='text' |
||||||
|
bsSize='sm' |
||||||
|
placeholder='' |
||||||
|
value='파일명.jpg' |
||||||
|
disabled |
||||||
|
/> |
||||||
|
<div className='user-phone-btn'> |
||||||
|
<Button size='sm' type='button' color='primary'> |
||||||
|
<span className='d-sm-inline-block'>다운로드</span> |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
|
||||||
|
<Col className='list-input' md='12'> |
||||||
|
<FormGroup className='feedback-input'> |
||||||
|
<Label for='test'> |
||||||
|
<span className='necessary'></span>내용 |
||||||
|
</Label> |
||||||
|
<Input |
||||||
|
className='faq-textarea' |
||||||
|
type='textarea' |
||||||
|
placeholder='' |
||||||
|
disabled |
||||||
|
value={adminDetailForm.content} |
||||||
|
onChange={e => {}} |
||||||
|
/> |
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
<Col className='list-input' md='4'> |
||||||
|
<FormGroup className='feedback-input'> |
||||||
|
<Label for='test'> |
||||||
|
<span className='necessary'></span>답변자 |
||||||
|
</Label> |
||||||
|
<Input |
||||||
|
type='text' |
||||||
|
bsSize='sm' |
||||||
|
placeholder='' |
||||||
|
value={adminDetailForm.anserUserNm} |
||||||
|
disabled |
||||||
|
/> |
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
<Col className='list-input' md='4'> |
||||||
|
<FormGroup> |
||||||
|
<Label for='test'>답변 일자</Label> |
||||||
|
<Input |
||||||
|
type='text' |
||||||
|
bsSize='sm' |
||||||
|
placeholder='' |
||||||
|
value={adminDetailForm.anserProcDt} |
||||||
|
disabled |
||||||
|
/> |
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
<Col className='list-input' md='4'> |
||||||
|
<FormGroup> |
||||||
|
<Label for='test'> |
||||||
|
<span className='necessary'></span>답변 상태 |
||||||
|
</Label> |
||||||
|
<div className='input-radio-inline'> |
||||||
|
<CustomInput |
||||||
|
type='radio' |
||||||
|
id='exampleCustomRadio' |
||||||
|
name='customRadio' |
||||||
|
inline |
||||||
|
label='답변대기' |
||||||
|
checked={adminDetailForm.anserStatus === 'N'} |
||||||
|
onClick={() => handlerChangeDetailForm('anserStatus', 'N')} |
||||||
|
readOnly |
||||||
|
/> |
||||||
|
<CustomInput |
||||||
|
type='radio' |
||||||
|
id='exampleCustomRadio2' |
||||||
|
name='customRadio' |
||||||
|
inline |
||||||
|
label='답변완료' |
||||||
|
checked={adminDetailForm.anserStatus === 'Y'} |
||||||
|
onClick={() => handlerChangeDetailForm('anserStatus', 'Y')} |
||||||
|
readOnly |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
<Col className='list-input' md='12'> |
||||||
|
<FormGroup className='feedback-input'> |
||||||
|
<Label for='test'> |
||||||
|
<span className='necessary'></span>답변 내용 |
||||||
|
</Label> |
||||||
|
<Input |
||||||
|
// className='faq-textarea'
|
||||||
|
type='textarea' |
||||||
|
placeholder='' |
||||||
|
value={adminDetailForm.anserContent} |
||||||
|
onChange={e => { |
||||||
|
const { value } = e.target; |
||||||
|
handlerChangeDetailForm('anserContent', value); |
||||||
|
}} |
||||||
|
/> |
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
</ModalBody> |
||||||
|
<ModalFooter> |
||||||
|
<div className='pal-form-btn'> |
||||||
|
<Button color='danger'>삭제</Button> |
||||||
|
<Button color='secondary' onClick={handlerDetailModal}> |
||||||
|
취소 |
||||||
|
</Button> |
||||||
|
<Button color='primary' onClick={handlerSubmitDetailForm}> |
||||||
|
저장 |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
</ModalFooter> |
||||||
|
</div> |
||||||
|
</Modal> |
||||||
|
); |
||||||
|
} |
@ -1,16 +1,99 @@ |
|||||||
import { Button, Card, CardBody, Col, Row, Input } from 'reactstrap'; |
import { useEffect, useState, useCallback } from 'react'; |
||||||
|
import { useSelector, useDispatch } from 'react-redux'; |
||||||
|
import { Col, Row } from 'reactstrap'; |
||||||
|
import { |
||||||
|
ADMIN_LIST, |
||||||
|
ADMIN_DETAIL |
||||||
|
} from '../../../modules/cstmrService/inquiry/action'; |
||||||
import QnaSearchBox from '../../../components/cstmrService/inquiry/QnaSearchBox'; |
import QnaSearchBox from '../../../components/cstmrService/inquiry/QnaSearchBox'; |
||||||
import QnaGrid from '../../../components/cstmrService/inquiry/QnaGrid'; |
import QnaGrid from '../../../components/cstmrService/inquiry/QnaGrid'; |
||||||
|
import QnaDetail from '../../../components/cstmrService/inquiry/QnaDetail'; |
||||||
|
|
||||||
|
const initalSearch = { |
||||||
|
category: '칭찬', |
||||||
|
anserStatus: 'N', |
||||||
|
createUserNm: '' |
||||||
|
}; |
||||||
|
|
||||||
export default function AdminInquiryContainer() { |
export default function AdminInquiryContainer() { |
||||||
|
const [searchData, setSearchData] = useState(initalSearch); |
||||||
|
const [isDetailModal, setIsDetailModal] = useState(false); |
||||||
|
const [adminDetailForm, setAdminDetailForm] = useState({}); |
||||||
|
const { adminList: lists, adminDetail } = useSelector( |
||||||
|
state => state.qnaState |
||||||
|
); |
||||||
|
const dispatch = useDispatch(); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
dispatch( |
||||||
|
ADMIN_LIST.request({ category: '', anserStatus: '', createUserNm: '' }) |
||||||
|
); |
||||||
|
}, []); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
setAdminDetailForm({ ...adminDetail }); |
||||||
|
}, [adminDetail]); |
||||||
|
|
||||||
|
// 관리자 문의 목록 조회
|
||||||
|
const handlerGetQnaList = () => { |
||||||
|
const { category, anserStatus, createUserNm } = searchData; |
||||||
|
dispatch(ADMIN_LIST.request({ category, anserStatus, createUserNm })); |
||||||
|
}; |
||||||
|
|
||||||
|
// 검색 조건 변경 handler
|
||||||
|
const handlerChangeSearchData = useCallback( |
||||||
|
(type, val) => { |
||||||
|
setSearchData({ ...searchData, [type]: val }); |
||||||
|
}, |
||||||
|
[searchData] |
||||||
|
); |
||||||
|
|
||||||
|
// 검색 버튼 클릭 hancler
|
||||||
|
const handlerSubmitSearch = () => { |
||||||
|
handlerGetQnaList(); |
||||||
|
}; |
||||||
|
|
||||||
|
// 상세보기 Form Modal handler
|
||||||
|
const handlerDetailModal = qnaSno => { |
||||||
|
if (!isDetailModal) { |
||||||
|
dispatch(ADMIN_DETAIL.request(qnaSno)); |
||||||
|
} |
||||||
|
setIsDetailModal(!isDetailModal); |
||||||
|
}; |
||||||
|
|
||||||
|
// 상세보기 Form value change handler
|
||||||
|
const handlerChangeDetailForm = useCallback( |
||||||
|
(type, val) => { |
||||||
|
setAdminDetailForm({ ...adminDetailForm, [type]: val }); |
||||||
|
}, |
||||||
|
[adminDetailForm] |
||||||
|
); |
||||||
|
|
||||||
|
// 상세보기 수정
|
||||||
|
const handlerSubmitDetailForm = () => { |
||||||
|
console.log('수정'); |
||||||
|
}; |
||||||
|
|
||||||
return ( |
return ( |
||||||
<Row> |
<Row> |
||||||
<Col sm='12' lg='12'> |
<Col sm='12' lg='12'> |
||||||
<QnaSearchBox /> |
<QnaSearchBox |
||||||
|
searchData={searchData} |
||||||
|
handlerChangeSearchData={handlerChangeSearchData} |
||||||
|
handlerSubmitSearch={handlerSubmitSearch} |
||||||
|
/> |
||||||
</Col> |
</Col> |
||||||
<Col sm='12' lg='12'> |
<Col sm='12' lg='12'> |
||||||
<QnaGrid /> |
<QnaGrid lists={lists} handlerDetailModal={handlerDetailModal} /> |
||||||
</Col> |
</Col> |
||||||
|
|
||||||
|
<QnaDetail |
||||||
|
isDetailModal={isDetailModal} |
||||||
|
handlerDetailModal={handlerDetailModal} |
||||||
|
adminDetailForm={adminDetailForm} |
||||||
|
handlerChangeDetailForm={handlerChangeDetailForm} |
||||||
|
handlerSubmitDetailForm={handlerSubmitDetailForm} |
||||||
|
/> |
||||||
</Row> |
</Row> |
||||||
); |
); |
||||||
} |
} |
||||||
|
@ -1,21 +1,39 @@ |
|||||||
import { AxiosError } from 'axios'; |
import { AxiosError } from 'axios'; |
||||||
import { createAsyncAction, ActionType, createAction } from 'typesafe-actions'; |
import { createAsyncAction, ActionType, createAction } from 'typesafe-actions'; |
||||||
|
|
||||||
import { IQnaAdminSearch, IQnaAdminList } from '../model'; |
import { IQnaAdminSearch, IQnaAdminList, IQnaAdminDetail } from '../model'; |
||||||
|
|
||||||
// 목록 조회
|
// 관리자 목록 조회
|
||||||
const ADMIN_LIST_REQUEST = 'cstmrService/qna/ADMIN_LIST_REQUEST'; |
const ADMIN_LIST_REQUEST = 'cstmrService/qna/ADMIN_LIST_REQUEST'; |
||||||
const ADMIN_LIST_SUCCESS = 'cstmrService/qna/ADMIN_LIST_SUCCESS'; |
const ADMIN_LIST_SUCCESS = 'cstmrService/qna/ADMIN_LIST_SUCCESS'; |
||||||
const ADMIN_LIST_FAILURE = 'cstmrService/qna/ADMIN_LIST_FAILURE'; |
const ADMIN_LIST_FAILURE = 'cstmrService/qna/ADMIN_LIST_FAILURE'; |
||||||
|
|
||||||
|
// 관리자 문의 상세보기
|
||||||
|
const ADMIN_DETAIL_REQUEST = 'cstmrService/qna/ADMIN_DETAIL_REQUEST'; |
||||||
|
const ADMIN_DETAIL_SUCCESS = 'cstmrService/qna/ADMIN_DETAIL_SUCCESS'; |
||||||
|
const ADMIN_DETAIL_FAILURE = 'cstmrService/qna/ADMIN_DETAIL_FAILURE'; |
||||||
|
|
||||||
|
// 관리자 문의 상세보기 초기화
|
||||||
|
const ADMIN_DETAIL_INITAL_ACTION = 'cstmrService/qna/ADMIN_DETAIL_INITAL'; |
||||||
|
|
||||||
export const ADMIN_LIST = createAsyncAction( |
export const ADMIN_LIST = createAsyncAction( |
||||||
ADMIN_LIST_REQUEST, |
ADMIN_LIST_REQUEST, |
||||||
ADMIN_LIST_SUCCESS, |
ADMIN_LIST_SUCCESS, |
||||||
ADMIN_LIST_FAILURE |
ADMIN_LIST_FAILURE |
||||||
)<IQnaAdminSearch, IQnaAdminList[], AxiosError>(); |
)<IQnaAdminSearch, IQnaAdminList[], AxiosError>(); |
||||||
|
|
||||||
|
export const ADMIN_DETAIL = createAsyncAction( |
||||||
|
ADMIN_DETAIL_REQUEST, |
||||||
|
ADMIN_DETAIL_SUCCESS, |
||||||
|
ADMIN_DETAIL_FAILURE |
||||||
|
)<number, IQnaAdminDetail, AxiosError>(); |
||||||
|
|
||||||
|
export const ADMIN_DETAIL_INITAL = createAction(ADMIN_DETAIL_INITAL_ACTION)(); |
||||||
|
|
||||||
const actions = { |
const actions = { |
||||||
ADMIN_LIST |
ADMIN_LIST, |
||||||
|
ADMIN_DETAIL, |
||||||
|
ADMIN_DETAIL_INITAL |
||||||
}; |
}; |
||||||
|
|
||||||
export type QnaAction = ActionType<typeof actions>; |
export type QnaAction = ActionType<typeof actions>; |
||||||
|
@ -1,11 +1,26 @@ |
|||||||
import axios from '../../../utils/customAxiosUtil'; |
import axios from '../../../utils/customAxiosUtil'; |
||||||
|
import qs from 'qs'; |
||||||
|
|
||||||
export const qnaAPI = { |
export const qnaAPI = { |
||||||
adminList: async ({ category, searchType, word }) => { |
adminList: async (data: { |
||||||
// const url =
|
category: string; |
||||||
// category === '전체'
|
anserStatus: string; |
||||||
// ? `api/bas/cns/faq?word=${word}`
|
createUserNm: string; |
||||||
// : `api/bas/cns/faq?category=${category}&word=${word}`;
|
}) => { |
||||||
return await axios.get(``); |
const params = {}; |
||||||
|
Object.keys(data).forEach(i => { |
||||||
|
if (data[i]) { |
||||||
|
params[`${i}`] = data[i]; |
||||||
|
} |
||||||
|
}); |
||||||
|
const queryString = qs.stringify(params, { |
||||||
|
addQueryPrefix: true, |
||||||
|
arrayFormat: 'repeat' |
||||||
|
}); |
||||||
|
|
||||||
|
return await axios.get(`api/cns/qna${queryString}`); |
||||||
|
}, |
||||||
|
adminDetail: async (qnaSno: number) => { |
||||||
|
return await axios.get(`api/cns/qna/${qnaSno}`); |
||||||
} |
} |
||||||
}; |
}; |
||||||
|
@ -1,11 +1,76 @@ |
|||||||
export interface IQnaState { |
export interface IQnaState { |
||||||
adminList: []; |
adminList: IQnaAdminList[]; |
||||||
|
adminDetail: IQnaAdminDetail; |
||||||
} |
} |
||||||
|
|
||||||
export interface IQnaAdminList {} |
export interface IQnaAdminList { |
||||||
|
qnaSno: number; |
||||||
|
targetSno: number; |
||||||
|
category: string; |
||||||
|
title: string; |
||||||
|
content: string; |
||||||
|
expsrYn: string; |
||||||
|
viewCnt: number; |
||||||
|
createUserNm: string; |
||||||
|
createUserId: string; |
||||||
|
createDt: string; |
||||||
|
updateUserId: string; |
||||||
|
updateDt: string; |
||||||
|
} |
||||||
|
|
||||||
export interface IQnaAdminSearch { |
export interface IQnaAdminSearch { |
||||||
category: string; |
category: string; |
||||||
searchType: string; |
anserStatus: string; |
||||||
word: string; |
createUserNm: string; |
||||||
|
} |
||||||
|
|
||||||
|
export interface IQnaFiles { |
||||||
|
fileSno: number; |
||||||
|
downloadUrl: string; |
||||||
|
fileName: string; |
||||||
|
} |
||||||
|
|
||||||
|
export interface IQnaAdminDetail { |
||||||
|
qnaSno: number; |
||||||
|
targetSno: number; |
||||||
|
category: string; |
||||||
|
title: string; |
||||||
|
content: string; |
||||||
|
anserContent: string; |
||||||
|
anserStatus: string; |
||||||
|
anserProcDt: string; |
||||||
|
anserUserNm: string; |
||||||
|
viewCnt: number; |
||||||
|
fileGroupNo: number; |
||||||
|
expsrYn: string; |
||||||
|
createUserNm: string; |
||||||
|
createUserId: string; |
||||||
|
createDt: string; |
||||||
|
updateUserId: string; |
||||||
|
updateDt: string; |
||||||
|
files: IQnaFiles[]; |
||||||
|
} |
||||||
|
|
||||||
|
export const initalState = { |
||||||
|
adminList: [], |
||||||
|
adminDetail: { |
||||||
|
qnaSno: 0, |
||||||
|
targetSno: 0, |
||||||
|
category: '', |
||||||
|
title: '', |
||||||
|
content: '', |
||||||
|
anserContent: '', |
||||||
|
anserStatus: '', |
||||||
|
anserProcDt: '', |
||||||
|
anserUserNm: '', |
||||||
|
viewCnt: 0, |
||||||
|
fileGroupNo: 0, |
||||||
|
expsrYn: '', |
||||||
|
createUserNm: '', |
||||||
|
createUserId: '', |
||||||
|
createDt: '', |
||||||
|
updateUserId: '', |
||||||
|
updateDt: '', |
||||||
|
files: [] |
||||||
} |
} |
||||||
|
}; |
||||||
|
@ -1,15 +1,28 @@ |
|||||||
import { createReducer } from 'typesafe-actions'; |
import { createReducer } from 'typesafe-actions'; |
||||||
import produce from 'immer'; |
import produce from 'immer'; |
||||||
import * as Actions from '../action'; |
import * as Actions from '../action'; |
||||||
import { IQnaState } from '../model'; |
import { IQnaState, initalState } from '../model'; |
||||||
|
|
||||||
export const faqReducer = createReducer<IQnaState, Actions.QnaAction>({ |
export const qnaReducer = createReducer<IQnaState, Actions.QnaAction>( |
||||||
adminList: [] |
initalState |
||||||
}) |
) |
||||||
// 목록
|
// 관리자 목록
|
||||||
.handleAction(Actions.ADMIN_LIST.success, (state, action) => |
.handleAction(Actions.ADMIN_LIST.success, (state, action) => |
||||||
produce(state, draft => { |
produce(state, draft => { |
||||||
const data = action.payload; |
const data = action.payload; |
||||||
// draft.adminList = data;
|
draft.adminList = data || []; |
||||||
|
}) |
||||||
|
) |
||||||
|
// 관리자 상세
|
||||||
|
.handleAction(Actions.ADMIN_DETAIL.success, (state, action) => |
||||||
|
produce(state, draft => { |
||||||
|
const data = action.payload; |
||||||
|
draft.adminDetail = { ...state.adminDetail, ...data }; |
||||||
|
}) |
||||||
|
) |
||||||
|
// 관리자 상세 초기화
|
||||||
|
.handleAction(Actions.ADMIN_DETAIL_INITAL, (state, action) => |
||||||
|
produce(state, draft => { |
||||||
|
draft.adminDetail = state.adminDetail; |
||||||
}) |
}) |
||||||
); |
); |
||||||
|
Loading…
Reference in new issue