This commit is contained in:
focp212@naver.com
2025-10-17 17:23:32 +09:00
parent ec16b6ec11
commit dc6d80eb81
8 changed files with 190 additions and 36 deletions

View File

@@ -33,8 +33,10 @@ export interface QnaItem {
cursorId?: number;
seq?: string;
statusCode?: string;
requestType?: string;
requestDate?: string;
requestName?: string;
answerDate?: string;
title?: string;
contents?: string;
answer?: string;
@@ -46,7 +48,7 @@ export interface QnaItemProps extends QnaItem {
};
export interface QnaSaveParams extends SupportParams {
counselType: string;
requestType: string;
requestName: string;
requestTel: string;
requestEmail: string;

View File

@@ -8,8 +8,10 @@ export const SupportQnaItem = ({
cursorId,
seq,
statusCode,
requestType,
requestDate,
requestName,
answerDate,
title,
contents,
answer
@@ -20,8 +22,10 @@ export const SupportQnaItem = ({
navigate(PATHS.support.qna.detail, {
state: {
statusCode,
requestType,
requestDate,
requestName,
answerDate,
title,
contents,
answer

View File

@@ -127,6 +127,14 @@
"01": "Save",
"02": "Submit Inquiry",
"03": "Completed"
},
"validation": {
"title": "Title is Required.",
"requestType": "RequestType is Required.",
"requestName": "RequestName is Required.",
"requestTel": "Phone number is Required.",
"requestEmail": "Invalid Email Type.",
"contents": "Contents is Required."
}
}
}

View File

@@ -127,6 +127,14 @@
"01": "저장",
"02": "문의",
"03": "답변완료"
},
"validation": {
"title": "제목은 필수 항목 입니다.",
"requestType": "문의유형은 필수 항목 입니다.",
"requestName": "요청자명은 필수 항목 입니다.",
"requestTel": "휴대폰번호은 필수 항목 입니다.",
"requestEmail": "이메일 형식이 맞지 않습니다.",
"contents": "문의내용은 필수 항목 입니다."
}
}
}

View File

@@ -10,19 +10,23 @@ import {
useSetFooterMode,
useSetOnBack
} from '@/widgets/sub-layout/use-sub-layout';
import { useTranslation } from 'react-i18next';
export const QnaDetailPage = () => {
const { navigate } = useNavigate();
const { t } = useTranslation();
const location = useLocation();
const [statusCode, setStatusCode] = useState<string>('');
const [requestDate, setRequestDate] = useState<string>('');
const [requestName, setRequestName] = useState<string>('');
const [requestType, setRequestType] = useState<string>('');
const [answerDate, setAnswerDate] = useState<string>('');
const [title, setTitle] = useState<string>('');
const [contents, setContents] = useState<string>('');
const [answer, setAnswer] = useState<string>('');
useSetHeaderTitle('1:1 문의');
useSetHeaderTitle(t('support.qna.title'));
useSetHeaderType(HeaderType.LeftArrow);
useSetFooterMode(false);
useSetOnBack(() => {
@@ -31,8 +35,10 @@ export const QnaDetailPage = () => {
useEffect(() => {
setStatusCode(location?.state.statusCode);
setRequestType(location?.state.requestType);
setRequestDate(location?.state.requestDate);
setRequestName(location?.state.requestName);
setAnswerDate(location?.state.answerDate);
setTitle(location?.state.title);
setContents(location?.state.contents);
setAnswer(location?.state.answer);
@@ -47,11 +53,11 @@ export const QnaDetailPage = () => {
<div className="inq-detail__head">
<div className="inq-detail__row">
<span className="inq-badge"></span>
<span className="inq-head-text bold">{ requestName }</span>
<span className="inq-head-text bold">{ title }</span>
</div>
<div className="inq-detail__row">
<span className="inq-badge"></span>
<span className="inq-head-text">/ </span>
<span className="inq-head-text">{ t(`support.qna.categories.${requestType}`) }</span>
</div>
<div className="inq-detail__row">
<span className="inq-badge"></span>
@@ -59,7 +65,7 @@ export const QnaDetailPage = () => {
</div>
<div className="inq-detail__row">
<span className="inq-badge"></span>
<span className="inq-head-text">2025.07.31</span>
<span className="inq-head-text">{ moment(answerDate).format('YYYY.MM.DD') }</span>
</div>
</div>
<div className="inq-detail__divider"></div>
@@ -70,18 +76,6 @@ export const QnaDetailPage = () => {
<div className="inq-detail__section">
<div className="inq-detail__section-title"> </div>
<div className="inq-detail__body" dangerouslySetInnerHTML={{ __html: contents || '' }}></div>
{/*
<div className="inq-detail__box">
<div className="inq-detail__kv">
<div className="k">제목</div>
<div className="v">{ title }</div>
</div>
<div className="inq-detail__kv">
<div className="k">내용</div>
<div className="v" dangerouslySetInnerHTML={{ __html: contents || '' }}></div>
</div>
</div>
*/}
</div>
</div>
</div>

View File

@@ -1,8 +1,8 @@
import { ChangeEvent, useEffect, useState } from 'react';
import { ChangeEvent, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PATHS } from '@/shared/constants/paths';
import { useNavigate } from '@/shared/lib/hooks/use-navigate';
import { HeaderType } from '@/entities/common/model/types';
import { DefaultRequestPagination, HeaderType } from '@/entities/common/model/types';
import { useQnaListMutation } from '@/entities/support/api/use-qna-list-mutation';
import { DEFAULT_PAGE_PARAM } from '@/entities/common/model/constant';
import { QnaItem, QnaListParams, QnaListResponse } from '@/entities/support/model/types';
@@ -14,18 +14,45 @@ import {
useSetOnBack
} from '@/widgets/sub-layout/use-sub-layout';
import { useStore } from '@/shared/model/store';
import useIntersectionObserver from '@/widgets/intersection-observer';
export const QnaListPage = () => {
const { navigate } = useNavigate();
const [onActionIntersect, setOnActionIntersect] = useState<boolean>(false);
const onIntersect: IntersectionObserverCallback = (entries: Array<IntersectionObserverEntry>) => {
entries.forEach((entry: IntersectionObserverEntry) => {
if(onActionIntersect){
if(entry.isIntersecting){
console.log('Element is now intersecting with the root.');
callList();
}
else{
console.log('Element is no longer intersecting with the root.');
}
}
});
};
const { setTarget } = useIntersectionObserver({
threshold: 1,
onIntersect
});
const midOptions = useStore.getState().UserStore.selectOptionsMids;
const userMid = useStore.getState().UserStore.mid;
const { t } = useTranslation();
const [mid, setMid] = useState<string>(userMid);
const [pageParam, setPageParam] = useState(DEFAULT_PAGE_PARAM);
const [pageParam, setPageParam] = useState<DefaultRequestPagination>(DEFAULT_PAGE_PARAM);
const [nextCursor, setNextCursor] = useState<string | null>(null);
const [statusCode, setStatusCode] = useState<string>(''); // 02, 03
const [resultList, setResultList] = useState<Array<QnaItem>>([]);
useSetHeaderTitle(t('support.qna.title'));
useSetHeaderType(HeaderType.LeftArrow);
useSetFooterMode(false);
@@ -35,17 +62,36 @@ export const QnaListPage = () => {
const { mutateAsync: qnaList } = useQnaListMutation();
const callList = () => {
setOnActionIntersect(false);
let listParams: QnaListParams = {
mid: mid,
statusCode: statusCode,
...{page: pageParam}
...{
page: pageParam
}
};
qnaList(listParams).then((rs: QnaListResponse) => {
setResultList(rs.content);
setResultList([
...resultList,
...rs.content
]);
if(rs.hasNext){
setNextCursor(rs.nextCursor);
setPageParam({
...pageParam,
cursor: rs.nextCursor
});
setOnActionIntersect(true);
}
else{
setNextCursor(null);
}
});
};
const getQnaList = () => {
let rs = [];
for(let i=0;i<resultList.length;i++){
@@ -55,8 +101,10 @@ export const QnaListPage = () => {
cursorId={ resultList[i]?.cursorId }
seq={ resultList[i]?.seq }
statusCode={ resultList[i]?.statusCode }
requestType={ resultList[i]?.requestType }
requestDate={ resultList[i]?.requestDate }
requestName={ resultList[i]?.requestName }
answerDate={ resultList[i]?.answerDate }
title={ resultList[i]?.title }
contents={ resultList[i]?.contents }
answer={ resultList[i]?.answer }
@@ -74,6 +122,10 @@ export const QnaListPage = () => {
callList();
}, [statusCode]);
useEffect(() => {
}, [resultList]);
return (
<>
<main>
@@ -93,7 +145,6 @@ export const QnaListPage = () => {
<option
key={ value.value }
value={ value.value }
selected={ (userMid === value.value)? true: false }
>{ value.name }</option>
))
}
@@ -113,6 +164,7 @@ export const QnaListPage = () => {
</div>
<div className="inq-list" >
{ getQnaList() }
<div ref={setTarget}></div>
</div>
</div>
<div className="apply-row">
@@ -124,6 +176,7 @@ export const QnaListPage = () => {
</div>
</div>
</main>
</>
);
};

View File

@@ -12,10 +12,13 @@ import {
import { useStore } from '@/shared/model/store';
import { useTranslation } from 'react-i18next';
import { PatternFormat } from 'react-number-format';
import { overlay } from 'overlay-kit';
import { Dialog } from '@/shared/ui/dialogs/dialog';
import { QnaSaveParams, QnaSaveResponse } from '@/entities/support/model/types';
export enum QnaRegisterPropsName {
Mid = 'Mid',
CounselType = 'CounselType',
RequestType = 'RequestType',
RequestName = 'RequestName',
RequestTel = 'RequestTel',
RequestEmail = 'RequestEmail',
@@ -29,7 +32,7 @@ export const QnaRegisterPage = () => {
const userMid = useStore.getState().UserStore.mid;
const [mid, setMid] = useState<string>(userMid);
const [counselType, setCounselType] = useState<string>('st');
const [requestType, setRequestType] = useState<string>('');
const [requestName, setRequestName] = useState<string>('');
const [requestTel, setRequestTel] = useState<string>('');
const [requestEmail, setRequestEmail] = useState<string>('');
@@ -53,8 +56,8 @@ export const QnaRegisterPage = () => {
if(key === QnaRegisterPropsName.Mid){
setMid(value);
}
else if(key === QnaRegisterPropsName.CounselType){
setCounselType(value);
else if(key === QnaRegisterPropsName.RequestType){
setRequestType(value);
}
else if(key === QnaRegisterPropsName.RequestName){
setRequestName(value);
@@ -72,18 +75,67 @@ export const QnaRegisterPage = () => {
setContents(value);
}
};
const showAlert = (msg: string) => {
overlay.open(({
isOpen,
close,
unmount
}) => {
return (
<Dialog
afterLeave={unmount}
open={isOpen}
onClose={close}
message={ msg }
buttonLabel={['확인']}
/>
);
});
}
const checkEmail = (email: string) => {
var re = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i;
if(email != '' && email != 'undefined' && re.test(email)){
return true;
}
else{
return false;
}
};
const callRegister = () => {
let params = {
if(!title){
showAlert(t('support.qna.validation.title'));
return;
}
else if(!requestType){
showAlert(t('support.qna.validation.requestType'));
return;
}
else if(!requestName){
showAlert(t('support.qna.validation.requestName'));
return;
}
else if(!requestTel){
showAlert(t('support.qna.validation.requestTel'));
return;
}
else if(requestEmail && !checkEmail(requestEmail)){
showAlert(t('support.qna.validation.requestEmail'));
return;
}
else if(!contents){
showAlert(t('support.qna.validation.contents'));
return;
}
let params: QnaSaveParams = {
mid: mid,
title: title,
counselType: counselType,
requestType: requestType,
requestName: requestName,
requestTel: requestTel,
requestEmail: requestEmail,
contents: contents,
};
qnaSave(params).then((rs) => {
qnaSave(params).then((rs: QnaSaveResponse) => {
alert('성공');
navigate(PATHS.support.qna.list);
});
@@ -118,9 +170,9 @@ export const QnaRegisterPage = () => {
</div>
<div className="inq-control">
<select
value={ counselType }
value={ requestType }
required= { true }
onChange={ (e: ChangeEvent<HTMLSelectElement>) => setInputValue(e, QnaRegisterPropsName.CounselType) }
onChange={ (e: ChangeEvent<HTMLSelectElement>) => setInputValue(e, QnaRegisterPropsName.RequestType) }
>
<option value="">{t('support.qna.categories.choose')}</option>
<option value="01">{t('support.qna.categories.01')}</option>
@@ -164,7 +216,7 @@ export const QnaRegisterPage = () => {
<div className="inq-label"> </div>
<div className="inq-control">
<input
type="text"
type="email"
placeholder="TEST123@nicepay.com"
value={ requestEmail }
onChange={ (e: ChangeEvent<HTMLInputElement>) => setInputValue(e, QnaRegisterPropsName.RequestEmail) }

View File

@@ -0,0 +1,33 @@
import { useEffect, useState } from "react";
interface useIntersectionObserverProps {
root?: null;
rootMargin?: string;
threshold?: number;
onIntersect: IntersectionObserverCallback;
}
const useIntersectionObserver = ({
root,
rootMargin = '0px',
threshold = 0,
onIntersect
}: useIntersectionObserverProps) => {
const [target, setTarget] = useState<HTMLElement | null | undefined>(null);
useEffect(() => {
if (!target) return;
const observer: IntersectionObserver = new IntersectionObserver(
onIntersect,
{ root, rootMargin, threshold }
);
observer.observe(target);
return () => observer.unobserve(target);
}, [onIntersect, root, rootMargin, target, threshold]);
return { setTarget };
};
export default useIntersectionObserver;