Browse Source

qna 관리자 문의 리스트, 상세보기, 수정 작업

pull/2/head
김장현 11 months ago
parent
commit
1104c6bd06
  1. 3
      .env.development
  2. 1
      src/components/cstmrService/faq/FaqForm.js
  3. 207
      src/components/cstmrService/inquiry/QnaDetail.js
  4. 123
      src/components/cstmrService/inquiry/QnaGrid.js
  5. 80
      src/components/cstmrService/inquiry/QnaSearchBox.js
  6. 5
      src/containers/cstmrService/faq/FaqContainer.js
  7. 89
      src/containers/cstmrService/inquiry/AdminInquiryContainer.js
  8. 24
      src/modules/cstmrService/inquiry/action/index.ts
  9. 27
      src/modules/cstmrService/inquiry/apis/index.ts
  10. 73
      src/modules/cstmrService/inquiry/model/index.ts
  11. 25
      src/modules/cstmrService/inquiry/reducers/index.ts
  12. 30
      src/modules/cstmrService/inquiry/sagas/index.ts
  13. 6
      src/redux/reducers/rootReducer.ts

3
.env.development

@ -4,7 +4,8 @@
# REACT_APP_IMAGE_HOST = https://palnet-file.s3.ap-northeast-2.amazonaws.com/
REACT_APP_HOST = http://211.253.11.189:8080/
REACT_APP_WS_HOST = ws://211.253.11.189:8081/ws
# REACT_APP_WS_HOST = ws://211.253.11.189:8081/ws
REACT_APP_WS_HOST = ws://pav.palntour.com:8081/ws
REACT_APP_IMAGE_HOST = https://palnet-file.s3.ap-northeast-2.amazonaws.com/
# Naver Search API HOST

1
src/components/cstmrService/faq/FaqForm.js

@ -11,7 +11,6 @@ import {
Label,
CustomInput
} from 'reactstrap';
import classnames from 'classnames';
export default function FaqForm({
isOpenFormModal,

207
src/components/cstmrService/inquiry/QnaDetail.js

@ -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>
);
}

123
src/components/cstmrService/inquiry/QnaGrid.js

@ -1,73 +1,68 @@
import { GridDatabase } from '../../crud/grid/GridDatatable';
import { Button, Card } from 'reactstrap';
const data = [
{
title: '문의 1',
category: '칭찬',
writer: 'palnet',
createDt: '2023-10-19 10:00',
status: '답변 대기'
}
];
const columns = [
{
id: 'sno',
name: '번호',
cell: (row, i) => <div>{i + 1}</div>
},
{
id: 'title',
name: '제목',
minWidth: '150px',
cell: (row, i) => <div>{row.title}</div>
},
{
id: 'category',
name: '문의 유형',
minWidth: '200px',
cell: row => {
return <div>{row.category}</div>;
export default function QnaGrid({ lists, handlerDetailModal }) {
const columns = [
{
id: 'sno',
name: '번호',
maxWidth: '50px',
cell: (row, i) => <div>{i + 1}</div>
},
{
id: 'title',
name: '제목',
minWidth: '300px',
cell: (row, i) => <div>{row.title}</div>
},
{
id: 'category',
name: '문의 유형',
minWidth: '100px',
cell: row => {
return <div>{row.category}</div>;
}
},
{
id: 'createUserNm',
name: '작성자',
minWidth: '100px',
cell: row => <div>{row.createUserNm}</div>
},
{
id: 'createDt',
name: '작성 일시',
cell: row => <div>{row.createDt}</div>
},
{
id: 'anserStatus',
name: '답변 상태',
minWidth: '50px',
cell: row => <div>{row.anserStatus}</div>
},
{
id: '',
name: '',
minWidth: '150px',
cell: row => (
<Button.Ripple
color='flat-primary'
onClick={() => {
handlerDetailModal(row.qnaSno);
}}
>
상세보기
</Button.Ripple>
)
}
},
{
id: 'writer',
name: '작성자',
minWidth: '200px',
cell: row => <div>{row.writer}</div>
},
{ id: 'createDt', name: '작성 일시', cell: row => <div>{row.createDt}</div> },
{
id: 'status',
name: '답변 상태',
cell: row => <div>{row.status}</div>
},
{
id: '',
name: '',
cell: row => (
<Button.Ripple
color='flat-primary'
onClick={() => {
location.href = '/cstmrService/admininquirywrite';
}}
>
상세보기
</Button.Ripple>
)
}
];
];
export default function QnaGrid() {
return (
<>
<div
className='cont-ti d-flex justify-content-between align-items-sm-center align-items-start flex-sm-row mt-2'
>
<div className='cont-ti d-flex justify-content-between align-items-sm-center align-items-start flex-sm-row mt-2'>
<div>
<h4>문의 목록</h4>
<span className='search-case'>검색결과 1</span>
<span className='search-case'>검색결과 {lists.length}</span>
</div>
<div className='d-flex align-items-center'></div>
</div>
@ -76,10 +71,10 @@ export default function QnaGrid() {
<div className='invoice-list-dataTable'>
<GridDatabase
title={'문의 목록'}
data={data}
data={lists}
columns={columns}
count={3}
total={3}
count={lists.length}
total={lists.length}
/>
</div>
</Card>

80
src/components/cstmrService/inquiry/QnaSearchBox.js

@ -1,9 +1,45 @@
import { Button, Card, CardBody, Col, Row, Input } from 'reactstrap';
import { Search } from 'react-feather';
import Flatpickr from 'react-flatpickr';
import moment from 'moment';
export default function QnaSearchBox() {
const categoryList = [
{
title: '칭찬',
value: '칭찬'
},
{
title: '불만',
value: '불만'
},
{
title: '문의',
value: '문의'
},
{
title: '제안',
value: '제안'
},
{
title: '기타',
value: '기타'
}
];
const anserStatusList = [
{
title: '답변 대기',
value: 'N'
},
{
title: '답변 완료',
value: 'Y'
}
];
export default function QnaSearchBox({
searchData,
handlerChangeSearchData,
handlerSubmitSearch
}) {
return (
<div>
<Row>
@ -13,7 +49,11 @@ export default function QnaSearchBox() {
<h4>검색조건</h4>
</div>
<div className='d-flex align-items-center'>
<Button.Ripple color='primary' size='sm' onClick={() => {}}>
<Button.Ripple
color='primary'
size='sm'
onClick={handlerSubmitSearch}
>
<Search size={16} />
검색
</Button.Ripple>
@ -33,17 +73,17 @@ export default function QnaSearchBox() {
<Input
type='select'
bsSize='sm'
// value={formData.category}
value={searchData.category}
onChange={e => {
const { value } = e.target;
// handlerChangeFormData('category', value);
handlerChangeSearchData('category', value);
}}
>
<option>칭찬</option>
<option>불만</option>
<option>문의</option>
<option>제안</option>
<option>기타</option>
{categoryList.map(i => (
<option key={i.value} value={i.value}>
{i.title}
</option>
))}
</Input>
</Col>
</Row>
@ -61,14 +101,17 @@ export default function QnaSearchBox() {
<Input
type='select'
bsSize='sm'
// value={formData.category}
value={searchData.anserStatus}
onChange={e => {
const { value } = e.target;
// handlerChangeFormData('category', value);
handlerChangeSearchData('anserStatus', value);
}}
>
<option>답변대기</option>
<option>답변완료</option>
{anserStatusList.map(i => (
<option key={i.value} value={i.value}>
{i.title}
</option>
))}
</Input>
</Col>
</Row>
@ -86,10 +129,13 @@ export default function QnaSearchBox() {
<Input
type='text'
bsSize='sm'
// value={formData.category}
value={searchData.createUserNm}
onChange={e => {
const { value } = e.target;
// handlerChangeFormData('category', value);
handlerChangeSearchData(
'createUserNm',
value
);
}}
/>
</Col>

5
src/containers/cstmrService/faq/FaqContainer.js

@ -242,7 +242,10 @@ export default function FaqContainer() {
<div>
<Button.Ripple
color='flat-primary'
onClick={() => handlerFaqModify(i)}
onClick={e => {
e.stopPropagation();
handlerFaqModify(i);
}}
>
수정하기
</Button.Ripple>

89
src/containers/cstmrService/inquiry/AdminInquiryContainer.js

@ -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 QnaGrid from '../../../components/cstmrService/inquiry/QnaGrid';
import QnaDetail from '../../../components/cstmrService/inquiry/QnaDetail';
const initalSearch = {
category: '칭찬',
anserStatus: 'N',
createUserNm: ''
};
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 (
<Row>
<Col sm='12' lg='12'>
<QnaSearchBox />
<QnaSearchBox
searchData={searchData}
handlerChangeSearchData={handlerChangeSearchData}
handlerSubmitSearch={handlerSubmitSearch}
/>
</Col>
<Col sm='12' lg='12'>
<QnaGrid />
<QnaGrid lists={lists} handlerDetailModal={handlerDetailModal} />
</Col>
<QnaDetail
isDetailModal={isDetailModal}
handlerDetailModal={handlerDetailModal}
adminDetailForm={adminDetailForm}
handlerChangeDetailForm={handlerChangeDetailForm}
handlerSubmitDetailForm={handlerSubmitDetailForm}
/>
</Row>
);
}

24
src/modules/cstmrService/inquiry/action/index.ts

@ -1,21 +1,39 @@
import { AxiosError } from 'axios';
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_SUCCESS = 'cstmrService/qna/ADMIN_LIST_SUCCESS';
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(
ADMIN_LIST_REQUEST,
ADMIN_LIST_SUCCESS,
ADMIN_LIST_FAILURE
)<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 = {
ADMIN_LIST
ADMIN_LIST,
ADMIN_DETAIL,
ADMIN_DETAIL_INITAL
};
export type QnaAction = ActionType<typeof actions>;

27
src/modules/cstmrService/inquiry/apis/index.ts

@ -1,11 +1,26 @@
import axios from '../../../utils/customAxiosUtil';
import qs from 'qs';
export const qnaAPI = {
adminList: async ({ category, searchType, word }) => {
// const url =
// category === '전체'
// ? `api/bas/cns/faq?word=${word}`
// : `api/bas/cns/faq?category=${category}&word=${word}`;
return await axios.get(``);
adminList: async (data: {
category: string;
anserStatus: string;
createUserNm: string;
}) => {
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}`);
}
};

73
src/modules/cstmrService/inquiry/model/index.ts

@ -1,11 +1,76 @@
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 {
category: string;
searchType: string;
word: string;
anserStatus: 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: []
}
};

25
src/modules/cstmrService/inquiry/reducers/index.ts

@ -1,15 +1,28 @@
import { createReducer } from 'typesafe-actions';
import produce from 'immer';
import * as Actions from '../action';
import { IQnaState } from '../model';
import { IQnaState, initalState } from '../model';
export const faqReducer = createReducer<IQnaState, Actions.QnaAction>({
adminList: []
})
// 목록
export const qnaReducer = createReducer<IQnaState, Actions.QnaAction>(
initalState
)
// 관리자 목록
.handleAction(Actions.ADMIN_LIST.success, (state, action) =>
produce(state, draft => {
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;
})
);

30
src/modules/cstmrService/inquiry/sagas/index.ts

@ -35,6 +35,34 @@ function* adminListSaga(action: ActionType<typeof Actions.ADMIN_LIST.request>) {
}
}
export function* faqSaga() {
function* adminDetailSaga(
action: ActionType<typeof Actions.ADMIN_DETAIL.request>
) {
try {
const payload = action.payload;
const res = yield call(Apis.qnaAPI.adminDetail, payload);
const { data, count, errorCode } = res;
if (errorCode) {
// 오류메시지 호출
yield put(
MessageActions.IS_ERROR({
errorCode: errorCode,
errorMessage: '처리중 오류가 발생하였습니다',
isHistoryBack: false,
isRefresh: false
})
);
return;
}
yield put(Actions.ADMIN_DETAIL.success(data));
} catch (error) {
yield put(Actions.ADMIN_DETAIL.failure(error));
}
}
export function* qnaSaga() {
yield takeEvery(Actions.ADMIN_LIST.request, adminListSaga);
yield takeEvery(Actions.ADMIN_DETAIL.request, adminDetailSaga);
}

6
src/redux/reducers/rootReducer.ts

@ -48,6 +48,8 @@ import { laancSaga } from '../../modules/laanc/sagas/laancSagas';
import laancReducer from '../../modules/laanc/reducers/laancReducers';
import { faqSaga } from '../../modules/cstmrService/faq/sagas';
import { faqReducer } from '../../modules/cstmrService/faq/reducers';
import { qnaSaga } from '../../modules/cstmrService/inquiry/sagas';
import { qnaReducer } from '../../modules/cstmrService/inquiry/reducers';
export interface StoreState {
controlGpState: ControlGpState;
@ -66,6 +68,7 @@ export function* saga() {
yield all([fork(findSaga)]);
yield all([fork(laancSaga)]);
yield all([fork(faqSaga)]);
yield all([fork(qnaSaga)]);
}
const rootReducer = combineReducers({
@ -96,7 +99,8 @@ const rootReducer = combineReducers({
ControlGpWeatherState: controlGpReducer,
flightState: flightReducer,
findState: findAccountReducer,
faqState: faqReducer
faqState: faqReducer,
qnaState: qnaReducer
});
export default rootReducer;

Loading…
Cancel
Save