Merge remote-tracking branch 'origin/iipbs/cs/dev'
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
> ⚠️ **중요**: 모든 것을 feature로 만들 필요는 없습니다.
|
||||
|
||||
Feature로 만드는 것이 좋은 지표:
|
||||
|
||||
- **여러 페이지에서 재사용됨**: 예를 들어, 여러 에디터가 있고 모두 댓글 기능이 있다면, 댓글은 재사용 가능한 feature입니다
|
||||
- **중요한 비즈니스 기능**: 신규 프로젝트 참여자가 Features 폴더를 보고 앱의 주요 기능을 빠르게 파악할 수 있어야 합니다
|
||||
|
||||
@@ -56,7 +57,7 @@ export function LoginForm() {
|
||||
password: ''
|
||||
});
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
const handleSubmit = async (e: React.SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
await loginUser(credentials);
|
||||
};
|
||||
@@ -104,11 +105,13 @@ export type { LoginCredentials } from './model/types';
|
||||
## 레이어 의존성 규칙
|
||||
|
||||
✅ **허용되는 의존성**:
|
||||
|
||||
- `shared` 레이어의 모듈
|
||||
- `entities` 레이어의 모듈
|
||||
- 같은 feature 슬라이스 내의 다른 세그먼트
|
||||
|
||||
❌ **금지된 의존성**:
|
||||
|
||||
- 다른 `features` 슬라이스 (feature 간 직접 의존 금지)
|
||||
- `widgets` 레이어
|
||||
- `pages` 레이어
|
||||
|
||||
@@ -14,7 +14,9 @@ interface SignupModalProps {
|
||||
}
|
||||
|
||||
function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
const [currentStep, setCurrentStep] = useState<'agreement' | 'signup'>('agreement');
|
||||
const [currentStep, setCurrentStep] = useState<"agreement" | "signup">(
|
||||
"agreement",
|
||||
);
|
||||
const [agreed, setAgreed] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
userId: "",
|
||||
@@ -47,7 +49,7 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
// 모달이 닫힐 때 초기화
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setCurrentStep('agreement');
|
||||
setCurrentStep("agreement");
|
||||
setAgreed(false);
|
||||
setFormData({
|
||||
userId: "",
|
||||
@@ -81,17 +83,19 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
|
||||
const handleNextStep = () => {
|
||||
if (agreed) {
|
||||
setCurrentStep('signup');
|
||||
setCurrentStep("signup");
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
|
||||
// 에러 초기화
|
||||
if (errors[name]) {
|
||||
setErrors(prev => ({ ...prev, [name]: "" }));
|
||||
setErrors((prev) => ({ ...prev, [name]: "" }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -102,20 +106,27 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
if (!formData.userId) newErrors.userId = "아이디를 입력해주세요.";
|
||||
if (!formData.userName) newErrors.userName = "이름을 입력해주세요.";
|
||||
if (!formData.password) newErrors.password = "비밀번호를 입력해주세요.";
|
||||
if (!formData.confirmPassword) newErrors.confirmPassword = "비밀번호 확인을 입력해주세요.";
|
||||
if (!formData.confirmPassword)
|
||||
newErrors.confirmPassword = "비밀번호 확인을 입력해주세요.";
|
||||
if (!formData.pswdHnt) newErrors.pswdHnt = "비밀번호 힌트를 입력해주세요.";
|
||||
if (!formData.laeSeCd) newErrors.laeSeCd = "법인구분을 선택해주세요.";
|
||||
if (!formData.ogdpSeCd) newErrors.ogdpSeCd = "기관구분을 선택해주세요.";
|
||||
|
||||
// 비밀번호 확인
|
||||
if (formData.password && formData.confirmPassword && formData.password !== formData.confirmPassword) {
|
||||
if (
|
||||
formData.password &&
|
||||
formData.confirmPassword &&
|
||||
formData.password !== formData.confirmPassword
|
||||
) {
|
||||
newErrors.confirmPassword = "비밀번호가 일치하지 않습니다.";
|
||||
}
|
||||
|
||||
// 비밀번호 정규식 검증
|
||||
const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/;
|
||||
const passwordRegex =
|
||||
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/;
|
||||
if (formData.password && !passwordRegex.test(formData.password)) {
|
||||
newErrors.password = "비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.";
|
||||
newErrors.password =
|
||||
"비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.";
|
||||
}
|
||||
|
||||
// 이메일 형식 검증
|
||||
@@ -124,8 +135,10 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
}
|
||||
|
||||
// 중복 체크 검증
|
||||
if (idAvailable === false) newErrors.userId = "이미 사용 중인 아이디입니다.";
|
||||
if (emailAvailable === false) newErrors.email = "이미 사용 중인 이메일입니다.";
|
||||
if (idAvailable === false)
|
||||
newErrors.userId = "이미 사용 중인 아이디입니다.";
|
||||
if (emailAvailable === false)
|
||||
newErrors.email = "이미 사용 중인 이메일입니다.";
|
||||
|
||||
setErrors(newErrors);
|
||||
const isValid = Object.keys(newErrors).length === 0;
|
||||
@@ -135,7 +148,7 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
|
||||
const checkUserIdDuplicate = async () => {
|
||||
if (!formData.userId) {
|
||||
setErrors(prev => ({ ...prev, userId: "아이디를 입력해주세요." }));
|
||||
setErrors((prev) => ({ ...prev, userId: "아이디를 입력해주세요." }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -147,19 +160,25 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
});
|
||||
const response = await signupClient.post<boolean>(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/existsUserId`,
|
||||
{ userId: formData.userId }
|
||||
{ userId: formData.userId },
|
||||
);
|
||||
|
||||
const exists = response.data;
|
||||
setIdAvailable(!exists);
|
||||
if (exists) {
|
||||
setErrors(prev => ({ ...prev, userId: "이미 사용 중인 아이디입니다." }));
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
userId: "이미 사용 중인 아이디입니다.",
|
||||
}));
|
||||
} else {
|
||||
setErrors(prev => ({ ...prev, userId: "" }));
|
||||
setErrors((prev) => ({ ...prev, userId: "" }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("아이디 중복 체크 오류:", error);
|
||||
setErrors(prev => ({ ...prev, userId: "아이디 중복 체크 중 오류가 발생했습니다." }));
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
userId: "아이디 중복 체크 중 오류가 발생했습니다.",
|
||||
}));
|
||||
} finally {
|
||||
setIsCheckingId(false);
|
||||
}
|
||||
@@ -167,7 +186,7 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
|
||||
const checkEmailDuplicate = async () => {
|
||||
if (!formData.email) {
|
||||
setErrors(prev => ({ ...prev, email: "이메일을 입력해주세요." }));
|
||||
setErrors((prev) => ({ ...prev, email: "이메일을 입력해주세요." }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -179,28 +198,34 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
});
|
||||
const response = await signupClient.post<boolean>(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/exists`,
|
||||
{ email: formData.email }
|
||||
{ email: formData.email },
|
||||
);
|
||||
|
||||
const exists = response.data;
|
||||
setEmailAvailable(!exists);
|
||||
if (exists) {
|
||||
setErrors(prev => ({ ...prev, email: "이미 사용 중인 이메일입니다." }));
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
email: "이미 사용 중인 이메일입니다.",
|
||||
}));
|
||||
} else {
|
||||
setErrors(prev => ({ ...prev, email: "" }));
|
||||
setErrors((prev) => ({ ...prev, email: "" }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("이메일 중복 체크 오류:", error);
|
||||
setErrors(prev => ({ ...prev, email: "이메일 중복 체크 중 오류가 발생했습니다." }));
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
email: "이메일 중복 체크 중 오류가 발생했습니다.",
|
||||
}));
|
||||
} finally {
|
||||
setIsCheckingEmail(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
const handleSubmit = async (e: React.SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
console.log("회원가입 버튼 클릭됨");
|
||||
|
||||
|
||||
if (!validateForm()) {
|
||||
console.log("폼 검증 실패");
|
||||
return;
|
||||
@@ -230,19 +255,19 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
|
||||
// SignupClient를 통해 익명사용자 회원가입 API 호출
|
||||
const signupClient = SignupClient.getInstance({
|
||||
baseURL: BASE_URL
|
||||
baseURL: BASE_URL,
|
||||
});
|
||||
const response = await signupClient.post(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/anonymouse/users/join`,
|
||||
signupData
|
||||
signupData,
|
||||
);
|
||||
|
||||
console.log("회원가입 성공:", response.data);
|
||||
|
||||
|
||||
// 성공 시 모달 닫기 및 콜백 호출
|
||||
onSuccess();
|
||||
onClose();
|
||||
|
||||
|
||||
// 폼 초기화
|
||||
setFormData({
|
||||
userId: "",
|
||||
@@ -267,13 +292,12 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
setErrors({});
|
||||
setIdAvailable(null);
|
||||
setEmailAvailable(null);
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error("회원가입 오류:", error);
|
||||
if (error instanceof Error) {
|
||||
setErrors(prev => ({ ...prev, submit: error.message }));
|
||||
setErrors((prev) => ({ ...prev, submit: error.message }));
|
||||
} else {
|
||||
setErrors(prev => ({ ...prev, submit: "회원가입에 실패했습니다." }));
|
||||
setErrors((prev) => ({ ...prev, submit: "회원가입에 실패했습니다." }));
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -286,36 +310,52 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h2>{currentStep === 'agreement' ? '개인정보제공동의' : '회원가입'}</h2>
|
||||
<button className="close-button" onClick={onClose}>×</button>
|
||||
<h2>
|
||||
{currentStep === "agreement" ? "개인정보제공동의" : "회원가입"}
|
||||
</h2>
|
||||
<button className="close-button" onClick={onClose}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{currentStep === 'agreement' ? (
|
||||
|
||||
{currentStep === "agreement" ? (
|
||||
<div className="agreement-content">
|
||||
<div className="agreement-text">
|
||||
<h3>개인정보 수집 및 이용 동의</h3>
|
||||
<p>회원가입을 위해 다음과 같이 개인정보를 수집 및 이용합니다.</p>
|
||||
|
||||
|
||||
<h4>1. 수집하는 개인정보 항목</h4>
|
||||
<ul>
|
||||
<li>필수항목: 아이디, 이름, 비밀번호, 비밀번호 힌트, 법인구분, 기관구분</li>
|
||||
<li>선택항목: 이메일, 휴대폰번호, 전화번호, 주소, 기관코드, 상세기관코드, 사업자등록번호</li>
|
||||
<li>
|
||||
필수항목: 아이디, 이름, 비밀번호, 비밀번호 힌트, 법인구분,
|
||||
기관구분
|
||||
</li>
|
||||
<li>
|
||||
선택항목: 이메일, 휴대폰번호, 전화번호, 주소, 기관코드,
|
||||
상세기관코드, 사업자등록번호
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<h4>2. 개인정보의 수집 및 이용목적</h4>
|
||||
<ul>
|
||||
<li>회원가입 및 관리</li>
|
||||
<li>서비스 제공 및 운영</li>
|
||||
<li>고객상담 및 문의응답</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<h4>3. 개인정보의 보유 및 이용기간</h4>
|
||||
<p>회원탈퇴 시까지 (단, 관련 법령에 따라 보존이 필요한 경우 해당 기간까지)</p>
|
||||
|
||||
<p>
|
||||
회원탈퇴 시까지 (단, 관련 법령에 따라 보존이 필요한 경우 해당
|
||||
기간까지)
|
||||
</p>
|
||||
|
||||
<h4>4. 동의 거부권 및 거부 시 불이익</h4>
|
||||
<p>개인정보 수집 및 이용에 대한 동의를 거부할 수 있으며, 동의 거부 시 회원가입이 제한됩니다.</p>
|
||||
<p>
|
||||
개인정보 수집 및 이용에 대한 동의를 거부할 수 있으며, 동의 거부
|
||||
시 회원가입이 제한됩니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="agreement-checkbox">
|
||||
<label>
|
||||
<input
|
||||
@@ -326,13 +366,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
<span>개인정보 수집 및 이용에 동의합니다.</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="agreement-actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="cancel-button"
|
||||
>
|
||||
<button type="button" onClick={onClose} className="cancel-button">
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
@@ -349,7 +385,7 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
<form onSubmit={handleSubmit} className="signup-form">
|
||||
<div className="form-section">
|
||||
<h3>기본 정보</h3>
|
||||
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="userId">아이디 *</label>
|
||||
@@ -373,8 +409,14 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
{isCheckingId ? "확인중..." : "중복확인"}
|
||||
</button>
|
||||
</div>
|
||||
{idAvailable === true && <span className="success-message">사용 가능한 아이디입니다.</span>}
|
||||
{errors.userId && <span className="error-message">{errors.userId}</span>}
|
||||
{idAvailable === true && (
|
||||
<span className="success-message">
|
||||
사용 가능한 아이디입니다.
|
||||
</span>
|
||||
)}
|
||||
{errors.userId && (
|
||||
<span className="error-message">{errors.userId}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -390,7 +432,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
placeholder="이름을 입력하세요"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.userName && <span className="error-message">{errors.userName}</span>}
|
||||
{errors.userName && (
|
||||
<span className="error-message">{errors.userName}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -416,8 +460,14 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
{isCheckingEmail ? "확인중..." : "중복확인"}
|
||||
</button>
|
||||
</div>
|
||||
{emailAvailable === true && <span className="success-message">사용 가능한 이메일입니다.</span>}
|
||||
{errors.email && <span className="error-message">{errors.email}</span>}
|
||||
{emailAvailable === true && (
|
||||
<span className="success-message">
|
||||
사용 가능한 이메일입니다.
|
||||
</span>
|
||||
)}
|
||||
{errors.email && (
|
||||
<span className="error-message">{errors.email}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -434,7 +484,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={20}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.password && <span className="error-message">{errors.password}</span>}
|
||||
{errors.password && (
|
||||
<span className="error-message">{errors.password}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -451,7 +503,11 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={20}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.confirmPassword && <span className="error-message">{errors.confirmPassword}</span>}
|
||||
{errors.confirmPassword && (
|
||||
<span className="error-message">
|
||||
{errors.confirmPassword}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -468,7 +524,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={10}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.pswdHnt && <span className="error-message">{errors.pswdHnt}</span>}
|
||||
{errors.pswdHnt && (
|
||||
<span className="error-message">{errors.pswdHnt}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -485,14 +543,16 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={100}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.pswdAns && <span className="error-message">{errors.pswdAns}</span>}
|
||||
{errors.pswdAns && (
|
||||
<span className="error-message">{errors.pswdAns}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<h3>연락처 정보</h3>
|
||||
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="mphn">휴대폰 번호</label>
|
||||
@@ -506,7 +566,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={11}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.mphn && <span className="error-message">{errors.mphn}</span>}
|
||||
{errors.mphn && (
|
||||
<span className="error-message">{errors.mphn}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -523,14 +585,16 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={11}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.telno && <span className="error-message">{errors.telno}</span>}
|
||||
{errors.telno && (
|
||||
<span className="error-message">{errors.telno}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<h3>주소 정보</h3>
|
||||
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="zip">우편번호</label>
|
||||
@@ -544,7 +608,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={5}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.zip && <span className="error-message">{errors.zip}</span>}
|
||||
{errors.zip && (
|
||||
<span className="error-message">{errors.zip}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -561,7 +627,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={200}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.addr && <span className="error-message">{errors.addr}</span>}
|
||||
{errors.addr && (
|
||||
<span className="error-message">{errors.addr}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -578,14 +646,16 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={200}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.daddr && <span className="error-message">{errors.daddr}</span>}
|
||||
{errors.daddr && (
|
||||
<span className="error-message">{errors.daddr}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<h3>기관 정보</h3>
|
||||
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="laeSeCd">법인구분 *</label>
|
||||
@@ -601,7 +671,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
<option value="02">민간기업</option>
|
||||
<option value="03">개인</option>
|
||||
</select>
|
||||
{errors.laeSeCd && <span className="error-message">{errors.laeSeCd}</span>}
|
||||
{errors.laeSeCd && (
|
||||
<span className="error-message">{errors.laeSeCd}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -621,7 +693,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
<option value="03">공공기관</option>
|
||||
<option value="04">민간기업</option>
|
||||
</select>
|
||||
{errors.ogdpSeCd && <span className="error-message">{errors.ogdpSeCd}</span>}
|
||||
{errors.ogdpSeCd && (
|
||||
<span className="error-message">{errors.ogdpSeCd}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -638,7 +712,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={10}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.instSeCd && <span className="error-message">{errors.instSeCd}</span>}
|
||||
{errors.instSeCd && (
|
||||
<span className="error-message">{errors.instSeCd}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -655,21 +731,25 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={10}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.dtlInstCd && <span className="error-message">{errors.dtlInstCd}</span>}
|
||||
{errors.dtlInstCd && (
|
||||
<span className="error-message">{errors.dtlInstCd}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{errors.submit && (
|
||||
<div className="form-row">
|
||||
<div className="error-message submit-error">{errors.submit}</div>
|
||||
<div className="error-message submit-error">
|
||||
{errors.submit}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCurrentStep('agreement')}
|
||||
onClick={() => setCurrentStep("agreement")}
|
||||
className="back-button"
|
||||
disabled={isLoading}
|
||||
>
|
||||
|
||||
@@ -305,7 +305,7 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
const handleSubmit = async (e: React.SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
console.log("회원가입 버튼 클릭됨 - handleSubmit");
|
||||
console.log("현재 폼 데이터:", formData);
|
||||
|
||||
@@ -26,7 +26,7 @@ function LoginPage() {
|
||||
console.log("Modal states - OTP:", showOtpModal, "2FA:", showTwoFAModal);
|
||||
}, [showOtpModal, showTwoFAModal]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = async (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setError("");
|
||||
@@ -43,44 +43,54 @@ function LoginPage() {
|
||||
if (isOtpEnabled) {
|
||||
// 2FA 로그인 - Proxy를 통해 처리 (2FA 전용 엔드포인트 사용)
|
||||
console.log("Starting 2FA login...");
|
||||
|
||||
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN_2FA}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
userId,
|
||||
password,
|
||||
provider: 'password'
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN_2FA}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
userId,
|
||||
password,
|
||||
provider: "password",
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
let errorMsg = errorData.message || '로그인 실패';
|
||||
let errorMsg = errorData.message || "로그인 실패";
|
||||
if (Array.isArray(errorData.errors) && errorData.errors.length > 0) {
|
||||
errorMsg = errorData.errors.map((e: { message: string }) => e.message).join(', ');
|
||||
errorMsg = errorData.errors
|
||||
.map((e: { message: string }) => e.message)
|
||||
.join(", ");
|
||||
}
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("2FA login response:", data);
|
||||
console.log("Response headers:", Object.fromEntries(response.headers.entries()));
|
||||
|
||||
console.log(
|
||||
"Response headers:",
|
||||
Object.fromEntries(response.headers.entries()),
|
||||
);
|
||||
|
||||
// 2FA 로그인 응답에는 tempToken과 registered2FA만 포함됨
|
||||
if (!data.tempToken) {
|
||||
throw new Error("Missing tempToken in 2FA login response");
|
||||
}
|
||||
|
||||
// 임시 토큰을 localStorage에 저장 (OTP 인증용)
|
||||
localStorage.setItem('tempToken', data.tempToken);
|
||||
localStorage.setItem("tempToken", data.tempToken);
|
||||
console.log("tempToken saved:", data.tempToken);
|
||||
|
||||
if (data.registered2FA) {
|
||||
console.log("User has 2FA registered, showing OTP modal");
|
||||
setShowOtpModal(true);
|
||||
} else {
|
||||
console.log("User doesn't have 2FA registered, showing 2FA setup modal");
|
||||
console.log(
|
||||
"User doesn't have 2FA registered, showing 2FA setup modal",
|
||||
);
|
||||
setShowTwoFAModal(true);
|
||||
}
|
||||
} else {
|
||||
@@ -91,7 +101,7 @@ function LoginPage() {
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error("Login error:", error);
|
||||
|
||||
|
||||
// 에러 처리
|
||||
if (error instanceof Error) {
|
||||
setError(error.message);
|
||||
@@ -218,25 +228,36 @@ function LoginPage() {
|
||||
<label htmlFor="checkTwoFA">OTP 인증</label>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="login_btn fx"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<button type="submit" className="login_btn fx" disabled={isLoading}>
|
||||
<span>{isLoading ? "로그인 중..." : "LOGIN"}</span>
|
||||
</button>
|
||||
{error && (
|
||||
<div className="error-message" style={{ color: "red", textAlign: "center", marginTop: "10px" }}>
|
||||
<div
|
||||
className="error-message"
|
||||
style={{ color: "red", textAlign: "center", marginTop: "10px" }}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{successMessage && (
|
||||
<div className="success-message" style={{ color: "green", textAlign: "center", marginTop: "10px" }}>
|
||||
<div
|
||||
className="success-message"
|
||||
style={{
|
||||
color: "green",
|
||||
textAlign: "center",
|
||||
marginTop: "10px",
|
||||
}}
|
||||
>
|
||||
{successMessage}
|
||||
</div>
|
||||
)}
|
||||
<div className="signup-link" style={{ textAlign: "center", marginTop: "15px" }}>
|
||||
<span style={{ color: "#666", fontSize: "14px" }}>계정이 없으신가요? </span>
|
||||
<div
|
||||
className="signup-link"
|
||||
style={{ textAlign: "center", marginTop: "15px" }}
|
||||
>
|
||||
<span style={{ color: "#666", fontSize: "14px" }}>
|
||||
계정이 없으신가요?{" "}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowSignupModal(true)}
|
||||
@@ -246,7 +267,7 @@ function LoginPage() {
|
||||
color: "#3b82f6",
|
||||
textDecoration: "underline",
|
||||
cursor: "pointer",
|
||||
fontSize: "14px"
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
회원가입
|
||||
@@ -282,8 +303,8 @@ function LoginPage() {
|
||||
종합환경연구단지)
|
||||
</span>
|
||||
<span>
|
||||
전화 : (시스템) 032-590-4321, 3752, 4314 / (통 계) 032-590-3723,
|
||||
3722
|
||||
전화 : (시스템) 032-590-4321, 3752, 4314 / (통 계)
|
||||
032-590-3723, 3722
|
||||
</span>
|
||||
<span>팩스 :032-590-3729</span>
|
||||
</p>
|
||||
|
||||
@@ -102,7 +102,7 @@ function UsersPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
|
||||
@@ -83,7 +83,7 @@ function FaqPage({ bbsId }: { bbsId: string }) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { LoginApi } from "@/entities/auth";
|
||||
import "./login.css";
|
||||
|
||||
function LoginPage() {
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = async (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
|
||||
@@ -33,7 +33,7 @@ function SearchPage() {
|
||||
queryFn: () => SearchApi.searchPost({ keyword: keyword, siteId }),
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
setKeyword(formData.get("keyword") as string);
|
||||
@@ -84,7 +84,8 @@ function SearchPage() {
|
||||
<TabsList className="mx-auto">
|
||||
<TabsTrigger value="all">
|
||||
전체 (
|
||||
{ (data?.contsList?.length ?? 0) + (data?.boardList?.totalElements ?? 0)}
|
||||
{(data?.contsList?.length ?? 0) +
|
||||
(data?.boardList?.totalElements ?? 0)}
|
||||
건)
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="contents">
|
||||
|
||||
@@ -134,7 +134,7 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { NtftalkHistoryPage } from "@/pages/ntftalk-history";
|
||||
|
||||
export default NtftalkHistoryPage;
|
||||
@@ -0,0 +1,164 @@
|
||||
# 화면설계서: 알림톡발송이력관리
|
||||
|
||||
## 1. 기본 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ---------- | -------------------------------------------- |
|
||||
| 버전 | 1.0 |
|
||||
| 프로세스명 | UI설계 |
|
||||
| 화면명 | 알림톡발송이력관리 |
|
||||
| 화면 유형 | [x] 조회 [ ] 입력 [ ] 삭제 [ ] 수정 [ ] 출력 |
|
||||
| 메뉴 경로 | home > 시스템관리 > 알림톡발송이력관리 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 화면 목적
|
||||
|
||||
알림톡 발송 이력을 **기간·내용·수신자·결과·시스템** 조건으로 검색하고, **발송 시스템·알림 내용·수신자·수신 시각·성공 여부**를 목록으로 확인한다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 화면 구성
|
||||
|
||||
### 3-1. 전체 레이아웃
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────┐
|
||||
│ 페이지 제목: 알림톡발송이력관리 브레드크럼 (우측 상단) │
|
||||
├────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 A] 검색 필터 │
|
||||
│ · 조회기간, 알림내용, 사용자, 결과, 시스템 │
|
||||
│ · [조회], [초기화] │
|
||||
├────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 B] 알림톡 발송 이력 그리드 │
|
||||
│ · 순번, 시스템, 알림내용, 수신자, 수신일, 결과 │
|
||||
│ · 페이지네이션 │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 영역 A: 검색 필터
|
||||
|
||||
### 4-1. 필드 정의
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| -------- | -------------------- | ---- | ------------- | ----------------------------------- | ------------------------------------------- |
|
||||
| 조회기간 | InputDateRangePicker | N | 1개월 전~오늘 | `dateFrom`, `dateTo` (`yyyy-MM-dd`) | `src/shared/ui/input-date-range-picker.tsx` |
|
||||
| 알림내용 | 텍스트 | N | 공백 | `content` | |
|
||||
| 사용자 | 텍스트 | N | 공백 | `recipientNum` | |
|
||||
| 결과 | 드롭다운 | N | 전체 | `successYn` | `SUCCESS` \| `FAIL` \| `ALL` |
|
||||
| 시스템 | SiteSelect | N | 전체 | `system` | `src/entities/site/ui/site-select.tsx` |
|
||||
|
||||
### 4-2. 버튼
|
||||
|
||||
| 버튼명 | 동작 |
|
||||
| ------ | ---------------------------------------------------------------- |
|
||||
| 조회 | 현재 필터로 영역 B 목록을 재조회한다. 페이지는 1페이지로 초기화. |
|
||||
| 초기화 | 검색 필드를 기본값으로 되돌린다. |
|
||||
|
||||
---
|
||||
|
||||
## 5. 영역 B: 알림톡 발송 이력 그리드
|
||||
|
||||
### 5-1. 그리드 컬럼
|
||||
|
||||
| 표시명 | Field | 비고 |
|
||||
| -------- | -------------- | ------------------------------------------------------------------------- |
|
||||
| 순번 | — | 클라이언트 표시용 인덱스(서버 필드 없음). 페이지네이션 시 오프셋 반영 |
|
||||
| 시스템 | `siteSystem` | 응답 시 `templateCode` 앞 4자 기반 파생(행정지원·하수도통계·포털·기타) |
|
||||
| 알림내용 | `content` | 긴 텍스트 말줄임 처리. 툴팁 적용하여 전체 텍스트 확인 가능. |
|
||||
| 수신자 | `recipientNum` | 실데이터·마스킹 규칙 **TBD** |
|
||||
| 수신일 | `dateRslt` | `YYYY-MM-DD HH:mm:ss` 형식 표시. |
|
||||
| 결과 | `successYn` | UI 표시: `SUCCESS` → 성공, `FAIL` → 실패(백엔드는 `reportCode` 기반 파생) |
|
||||
|
||||
- 행 클릭 UX 없음.
|
||||
|
||||
---
|
||||
|
||||
## 6. API/DTO 명세
|
||||
|
||||
### 6-1. 공통
|
||||
|
||||
- 목록 응답: Spring `Page` 직렬화 형태 — 프론트 타입 `PageableList<NtftalkMmtLogHistory>` (`content`, `totalElements`, `totalPages`, `number`, `size` 등).
|
||||
- 목록 요청 쿼리: `Pageable` 표준 파라미터 `page`, `size`, `sort` (`PageableParams`).
|
||||
|
||||
### 6-2. 알림톡 발송 이력 API
|
||||
|
||||
Base: `/ntftalk-service/api/v1/ata/mmt-log-history`
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API(클라이언트 함수명) |
|
||||
| ----------------- | ------ | ----------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ----------------------------- |
|
||||
| 목록 조회(페이징) | GET | `/ata/mmt-log-history` | Query: `AtaMmtLogHistorySearchDto` 필드 + `Pageable`(`page`,`size`,`sort`) | `Page<AtaMmtLogHistoryResponseDto>` → `NtftalkMmtLogHistoryPageList` | `getNtftalkMmtLogHistoryPage` |
|
||||
| 상세 조회 | GET | `/ata/mmt-log-history/{mtPr}` | Path: `mtPr` | `AtaMmtLogHistoryResponseDto` → `NtftalkMmtLogHistory` | `getNtftalkMmtLogHistory` |
|
||||
| 등록 | POST | `/ata/mmt-log-history` | Body: `AtaMmtLogHistorySaveRequestDto` → `NtftalkMmtLogHistorySavePayload` | `AtaMmtLogHistoryResponseDto` | `saveNtftalkMmtLogHistory` |
|
||||
| 수정 | PUT | `/ata/mmt-log-history/{mtPr}` | Path: `mtPr`, Body: `AtaMmtLogHistoryUpdateRequestDto` → `NtftalkMmtLogHistoryUpdatePayload` | `AtaMmtLogHistoryResponseDto` | `updateNtftalkMmtLogHistory` |
|
||||
| 삭제 | DELETE | `/ata/mmt-log-history/{mtPr}` | Path: `mtPr` | 없음(204) | `deleteNtftalkMmtLogHistory` |
|
||||
|
||||
#### 검색 Query ↔ TypeScript (`NtftalkMmtLogHistorySearchParams`)
|
||||
|
||||
| Query (Java 필드) | TS 필드 | 설명 |
|
||||
| ----------------- | --------------- | ------------------------------------ |
|
||||
| `subject` | `subject?` | 메시지 제목 |
|
||||
| `content` | `content?` | 전송 메시지 내용 |
|
||||
| `recipientNum` | `recipientNum?` | 수신자 전화번호 |
|
||||
| `msgStatus` | `msgStatus?` | `1` 전송대기, `2` 결과대기, `3` 완료 |
|
||||
| `reportCode` | `reportCode?` | 전송 결과 코드(`1000`: 성공) |
|
||||
| `dateFrom` | `dateFrom?` | `yyyy-MM-dd` |
|
||||
| `dateTo` | `dateTo?` | `yyyy-MM-dd` |
|
||||
| `system` | `system?` | `ADSS` \| `CWMS` \| `PORT` \| `ALL` |
|
||||
| `successYn` | `successYn?` | `SUCCESS` \| `FAIL` \| `ALL` |
|
||||
|
||||
#### 응답 행 ↔ TypeScript (`NtftalkMmtLogHistory`)
|
||||
|
||||
백엔드 `AtaMmtLogHistoryResponseDto`와 동일 필드명: `mtPr`, `mtRefkey`, `priority`, `dateClientReq`, `subject`, `content`, `callback`, `msgStatus`, `recipientNum`, `dateMtSent`, `dateRslt`, `dateMtReport`, `reportCode`, `countryCode`, `msgType`, `regDate`, `senderKey`, `templateCode`, `siteSystem`, `successYn`.
|
||||
|
||||
---
|
||||
|
||||
## 7. UI 구현 가이드
|
||||
|
||||
### 7-1. 기술 스택·원칙
|
||||
|
||||
- **UI**: `@common/components` 공통 컴포넌트
|
||||
- **테이블**: `@tanstack/react-table` + `useDataTable`(`src/shared/lib/use-data-table.tsx`)
|
||||
- **데이터**: `@tanstack/react-query` — 목록은 `getNtftalkMmtLogHistoryPage`, 무한 스크롤 vs 페이지 UI는 **TBD**
|
||||
- **레이아웃**: App Router `app/(with-base-layout)/ntftalk-history/page.tsx`에서 페이지 UI 마운트
|
||||
|
||||
### 7-2. 모듈·파일 배치
|
||||
|
||||
| 구분 | 경로 |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| 라우트 엔트리 | `apps/cs/app/(with-base-layout)/ntftalk-history/page.tsx` |
|
||||
| 페이지 셸 | `apps/cs/src/pages/ntftalk-history/ui/ntftalk-history-page.tsx` |
|
||||
| 컬럼 정의 | `apps/cs/src/pages/ntftalk-history/model/*-table-columns.tsx` |
|
||||
| API·타입 | `apps/cs/src/entities/ntftalk/model/ntftalk-mmt-log-history-interface.ts`, `api/ntftalk-mmt-log-history-service.ts` |
|
||||
| 엔드포인트 상수 | `apps/cs/src/shared/config/api.ts` — `ENDPOINT.NTFTALK_SERVICE.MMT_LOG_HISTORY` |
|
||||
|
||||
### 7-3. 영역별 UI 매핑
|
||||
|
||||
| 영역 | 화면 요소 | 컴포넌트·훅 | 구현 비고 |
|
||||
| ---- | ------------ | ------------------------------------------------ | ---------------------------- |
|
||||
| A | 검색 폼 | `Input`, `Label`, `Select`, `Button` | |
|
||||
| B | 이력 그리드 | `useDataTable` + `ColumnDef`, `manualPagination` | |
|
||||
| 공통 | 로딩·빈 목록 | `LoadingWithAnimation`, 빈 데이터 안내 | 기타 목록 페이지와 동일 수준 |
|
||||
|
||||
### 7-4. 데이터 흐름
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 최초 진입
|
||||
CALL 알림톡발송이력목록조회(기본 검색 조건, page=0, size=10)
|
||||
DISPLAY 알림톡 발송 이력 그리드 (영역 B)
|
||||
|
||||
ON [조회] 버튼 CLICK DO
|
||||
CALL 알림톡발송이력목록조회(조회기간→dateFrom/dateTo, content, recipientNum, successYn, system, page=0)
|
||||
DISPLAY 알림톡 발송 이력 그리드 (영역 B)
|
||||
END ON
|
||||
|
||||
ON [초기화] 버튼 CLICK DO
|
||||
RESET 검색 조건 → 기본값
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
@@ -1,6 +1,7 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import baseEsLintConfig from "../../eslint.config.mjs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -11,6 +12,7 @@ const compat = new FlatCompat({
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
...baseEsLintConfig,
|
||||
{
|
||||
rules: {
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import {
|
||||
type NtftalkMmtLogHistory,
|
||||
type NtftalkMmtLogHistoryPageList,
|
||||
type NtftalkMmtLogHistorySavePayload,
|
||||
type NtftalkMmtLogHistorySearchParams,
|
||||
type NtftalkMmtLogHistoryService,
|
||||
type NtftalkMmtLogHistoryUpdatePayload,
|
||||
} from "../model/ntftalk-mmt-log-history-interface";
|
||||
|
||||
class NtftalkMmtLogHistoryApiService implements NtftalkMmtLogHistoryService {
|
||||
async getNtftalkMmtLogHistoryPage(
|
||||
params: NtftalkMmtLogHistorySearchParams,
|
||||
): Promise<NtftalkMmtLogHistoryPageList> {
|
||||
const response = await client.get<NtftalkMmtLogHistoryPageList>(
|
||||
`${ENDPOINT.NTFTALK_SERVICE.MMT_LOG_HISTORY}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getNtftalkMmtLogHistory(mtPr: number): Promise<NtftalkMmtLogHistory> {
|
||||
const response = await client.get<NtftalkMmtLogHistory>(
|
||||
`${ENDPOINT.NTFTALK_SERVICE.MMT_LOG_HISTORY}/${mtPr}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async saveNtftalkMmtLogHistory(
|
||||
payload: NtftalkMmtLogHistorySavePayload,
|
||||
): Promise<NtftalkMmtLogHistory> {
|
||||
const response = await client.post<NtftalkMmtLogHistory>(
|
||||
`${ENDPOINT.NTFTALK_SERVICE.MMT_LOG_HISTORY}`,
|
||||
payload,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateNtftalkMmtLogHistory(
|
||||
mtPr: number,
|
||||
payload: NtftalkMmtLogHistoryUpdatePayload,
|
||||
): Promise<NtftalkMmtLogHistory> {
|
||||
const response = await client.put<NtftalkMmtLogHistory>(
|
||||
`${ENDPOINT.NTFTALK_SERVICE.MMT_LOG_HISTORY}/${mtPr}`,
|
||||
payload,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async deleteNtftalkMmtLogHistory(mtPr: number): Promise<void> {
|
||||
await client.delete(
|
||||
`${ENDPOINT.NTFTALK_SERVICE.MMT_LOG_HISTORY}/${mtPr}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const NtftalkMmtLogHistoryApi = new NtftalkMmtLogHistoryApiService();
|
||||
@@ -1,6 +1,7 @@
|
||||
export { NtftalkVarApi } from "./api/ntftalk-var-service";
|
||||
export { NtftalkTempApi } from "./api/ntftalk-temp-service";
|
||||
export { NtftalkBiztalkApi } from "./api/ntftalk-biztalk-service";
|
||||
export { NtftalkMmtLogHistoryApi } from "./api/ntftalk-mmt-log-history-service";
|
||||
export type {
|
||||
NtftalkVar,
|
||||
NtftalkVarPageList,
|
||||
@@ -27,3 +28,10 @@ export type {
|
||||
NtftalkTempSearchParams,
|
||||
NtftalkTempUpdatePayload,
|
||||
} from "./model/ntftalk-temp-interface";
|
||||
export type {
|
||||
NtftalkMmtLogHistory,
|
||||
NtftalkMmtLogHistoryPageList,
|
||||
NtftalkMmtLogHistorySavePayload,
|
||||
NtftalkMmtLogHistorySearchParams,
|
||||
NtftalkMmtLogHistoryUpdatePayload,
|
||||
} from "./model/ntftalk-mmt-log-history-interface";
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface NtftalkMmtLogHistorySearchParams extends PageableParams {
|
||||
subject?: string;
|
||||
content?: string;
|
||||
recipientNum?: string;
|
||||
/** 1:전송대기, 2:결과대기, 3:완료 */
|
||||
msgStatus?: string;
|
||||
/** 1000:성공 */
|
||||
reportCode?: string;
|
||||
/** yyyy-MM-dd */
|
||||
dateFrom?: string;
|
||||
/** yyyy-MM-dd */
|
||||
dateTo?: string;
|
||||
/** ADSS | CWMS | PORT | ALL */
|
||||
system?: string;
|
||||
/** SUCCESS | FAIL | ALL */
|
||||
successYn?: string;
|
||||
}
|
||||
|
||||
export interface NtftalkMmtLogHistory {
|
||||
mtPr: number;
|
||||
mtRefkey?: string | null;
|
||||
priority?: string | null;
|
||||
dateClientReq?: string | null;
|
||||
subject?: string | null;
|
||||
content?: string | null;
|
||||
callback?: string | null;
|
||||
msgStatus?: string | null;
|
||||
recipientNum?: string | null;
|
||||
dateMtSent?: string | null;
|
||||
dateRslt?: string | null;
|
||||
dateMtReport?: string | null;
|
||||
reportCode?: string | null;
|
||||
countryCode?: string | null;
|
||||
msgType?: number | null;
|
||||
regDate?: string | null;
|
||||
senderKey?: string | null;
|
||||
templateCode?: string | null;
|
||||
siteSystem?: string | null;
|
||||
successYn?: string | null;
|
||||
}
|
||||
|
||||
export interface NtftalkMmtLogHistorySavePayload {
|
||||
mtPr: number;
|
||||
mtRefkey?: string;
|
||||
priority?: string;
|
||||
dateClientReq?: string;
|
||||
subject: string;
|
||||
content: string;
|
||||
callback: string;
|
||||
msgStatus?: string;
|
||||
recipientNum: string;
|
||||
reportCode?: string;
|
||||
countryCode?: string;
|
||||
msgType?: number;
|
||||
senderKey: string;
|
||||
templateCode?: string;
|
||||
responseMethod?: string;
|
||||
}
|
||||
|
||||
export interface NtftalkMmtLogHistoryUpdatePayload {
|
||||
content: string;
|
||||
recipientNum: string;
|
||||
msgStatus?: string;
|
||||
reportCode?: string;
|
||||
}
|
||||
|
||||
export type NtftalkMmtLogHistoryPageList = PageableList<NtftalkMmtLogHistory>;
|
||||
|
||||
export interface NtftalkMmtLogHistoryService {
|
||||
getNtftalkMmtLogHistoryPage(
|
||||
params: NtftalkMmtLogHistorySearchParams,
|
||||
): Promise<NtftalkMmtLogHistoryPageList>;
|
||||
getNtftalkMmtLogHistory(mtPr: number): Promise<NtftalkMmtLogHistory>;
|
||||
saveNtftalkMmtLogHistory(
|
||||
payload: NtftalkMmtLogHistorySavePayload,
|
||||
): Promise<NtftalkMmtLogHistory>;
|
||||
updateNtftalkMmtLogHistory(
|
||||
mtPr: number,
|
||||
payload: NtftalkMmtLogHistoryUpdatePayload,
|
||||
): Promise<NtftalkMmtLogHistory>;
|
||||
deleteNtftalkMmtLogHistory(mtPr: number): Promise<void>;
|
||||
}
|
||||
@@ -101,7 +101,7 @@ function FaqPage({ bbsId }: { bbsId: string }) {
|
||||
alert("삭제되었습니다.");
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
};
|
||||
|
||||
@@ -174,7 +174,7 @@ function AddAdminDialog() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSearch = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setPagination({ pageIndex: 0, pageSize: 10 });
|
||||
|
||||
@@ -125,7 +125,7 @@ function AdminPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
|
||||
@@ -75,7 +75,7 @@ function ApiLogPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
|
||||
@@ -9,7 +9,7 @@ import "./login.css";
|
||||
function LoginPage() {
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = async (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setErrorMessage(""); // 에러 메시지 초기화
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ function BannerPage() {
|
||||
}),
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
|
||||
@@ -51,7 +51,7 @@ function BbsPage() {
|
||||
}),
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
|
||||
@@ -107,7 +107,7 @@ function BoardStatisticsPage() {
|
||||
setPresetDateRange("");
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const params: BoardStatisticsParams = {
|
||||
siteId,
|
||||
|
||||
@@ -65,7 +65,7 @@ function LwpoCodeSection({ upCd }: LwpoCodeSectionProps) {
|
||||
dataTableOptions: { viewPagination: true },
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
|
||||
@@ -40,7 +40,7 @@ function UpCodeSection({ handleSelectedUpCd }: UpCodeSectionProps) {
|
||||
handleSelectedUpCd(row.upCd);
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
|
||||
@@ -38,7 +38,7 @@ function ContentsPage() {
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
|
||||
@@ -65,7 +65,7 @@ function FilePage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
|
||||
@@ -149,7 +149,7 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSearch = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setPagination({
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button,Input, Label } from "@common/components";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { GroupUsersApi } from "@/entities/group-user";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useDataTable } from "@/shared/lib";
|
||||
@@ -33,8 +33,8 @@ function GroupUsersSection({ groupId }: GroupUsersSectionProps) {
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const [userNm, setUserNm] = useState("");
|
||||
|
||||
const [userNm, setUserNm] = useState("");
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const query = useQuery({
|
||||
@@ -50,7 +50,7 @@ function GroupUsersSection({ groupId }: GroupUsersSectionProps) {
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
@@ -116,27 +116,27 @@ function GroupUsersSection({ groupId }: GroupUsersSectionProps) {
|
||||
return (
|
||||
<div className="flex-1">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label htmlFor="" className="mr-2 text-label">
|
||||
이름
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
|
||||
value={userNm}
|
||||
onChange={(e) => setUserNm(e.target.value)}
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
className="h-[3.2rem]"
|
||||
onClick={handleReset}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
</form>
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label htmlFor="" className="mr-2 text-label">
|
||||
이름
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
|
||||
value={userNm}
|
||||
onChange={(e) => setUserNm(e.target.value)}
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
className="h-[3.2rem]"
|
||||
onClick={handleReset}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
|
||||
@@ -8,13 +8,13 @@ import {
|
||||
} from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { GroupApi , type GroupSearchParams} from "@/entities/group";
|
||||
import { GroupApi, type GroupSearchParams } from "@/entities/group";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { createColumns } from "../model/groups-table-columns";
|
||||
import { GroupsDialog } from "./groups-dialog";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
|
||||
type GroupsSectionProps = {
|
||||
handleSelectedGroup: (groupId: string) => void;
|
||||
@@ -29,14 +29,12 @@ function GroupsSection({ handleSelectedGroup }: GroupsSectionProps) {
|
||||
const [groupNm, setGroupNm] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<GroupSearchParams>({
|
||||
groupNm: "",
|
||||
delYn :"N",
|
||||
sort : "groupId,desc",
|
||||
delYn: "N",
|
||||
sort: "groupId,desc",
|
||||
});
|
||||
|
||||
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUPS,searchParams, pagination],
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUPS, searchParams, pagination],
|
||||
queryFn: () =>
|
||||
GroupApi.findPageGroup({
|
||||
...searchParams,
|
||||
@@ -64,7 +62,7 @@ function GroupsSection({ handleSelectedGroup }: GroupsSectionProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
@@ -81,7 +79,7 @@ function GroupsSection({ handleSelectedGroup }: GroupsSectionProps) {
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
groupNm: "",
|
||||
delYn:"N"
|
||||
delYn: "N",
|
||||
});
|
||||
};
|
||||
const handleRowClick = (row: { groupId: string }) => {
|
||||
@@ -109,41 +107,41 @@ function GroupsSection({ handleSelectedGroup }: GroupsSectionProps) {
|
||||
return (
|
||||
<div className="flex-1">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label htmlFor="" className="mr-2 text-label">
|
||||
그룹명
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
|
||||
value={groupNm}
|
||||
onChange={(e) => setGroupNm(e.target.value)}
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
className="h-[3.2rem]"
|
||||
onClick={handleReset}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
<Label htmlFor="delYn" className="ml-4 mr-2 text-label">
|
||||
삭제여부
|
||||
</Label>
|
||||
<input
|
||||
id="delYn"
|
||||
type="checkbox"
|
||||
checked={searchParams.delYn === "Y"}
|
||||
onChange={(e) =>
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
delYn: e.target.checked ? "Y" : "N",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label htmlFor="" className="mr-2 text-label">
|
||||
그룹명
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
|
||||
value={groupNm}
|
||||
onChange={(e) => setGroupNm(e.target.value)}
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
className="h-[3.2rem]"
|
||||
onClick={handleReset}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
<Label htmlFor="delYn" className="ml-4 mr-2 text-label">
|
||||
삭제여부
|
||||
</Label>
|
||||
<input
|
||||
id="delYn"
|
||||
type="checkbox"
|
||||
checked={searchParams.delYn === "Y"}
|
||||
onChange={(e) =>
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
delYn: e.target.checked ? "Y" : "N",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
|
||||
@@ -95,7 +95,7 @@ function MenuAuthorityDialog({ groupId }: MenuAuthorityDialogProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const keyword = formData.get("keyword") as string;
|
||||
|
||||
@@ -46,7 +46,7 @@ function ImagePage() {
|
||||
}),
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
|
||||
@@ -4,7 +4,17 @@ import "@common/assets/styles/grid.css";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button, Input, Label, LoadingWithAnimation, Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@common/components";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
LoadingWithAnimation,
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { getCoreRowModel } from "@tanstack/react-table";
|
||||
import { LinkApi, type Link, type LinkSearchParam } from "@/entities/link";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
@@ -14,147 +24,161 @@ import { useDataTable } from "@/shared/lib";
|
||||
import LinkExpiredAlert from "./LinkExpiredAlert";
|
||||
|
||||
function LinkPage() {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [linkSn, setLinkSn] = useState<string>('');
|
||||
const [linkNm, setLinkNm] = useState<string>('');
|
||||
const [linkClfsCd, setLinkClfsCd] = useState<string>('');
|
||||
const [linkSn, setLinkSn] = useState<string>("");
|
||||
const [linkNm, setLinkNm] = useState<string>("");
|
||||
const [linkClfsCd, setLinkClfsCd] = useState<string>("");
|
||||
|
||||
const [linkSearchParam, setLinkSearchParam] = useState<LinkSearchParam>({
|
||||
linkSn: '',
|
||||
linkNm: '',
|
||||
linkClfsCd: '',
|
||||
const [linkSearchParam, setLinkSearchParam] = useState<LinkSearchParam>({
|
||||
linkSn: "",
|
||||
linkNm: "",
|
||||
linkClfsCd: "",
|
||||
});
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [
|
||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`,
|
||||
linkSearchParam,
|
||||
],
|
||||
queryFn: () => LinkApi.getLinkList(linkSearchParam),
|
||||
});
|
||||
|
||||
const handleResetSearchParam = () => {
|
||||
setLinkSn("");
|
||||
setLinkNm("");
|
||||
setLinkClfsCd("ALL");
|
||||
setLinkSearchParam({
|
||||
...linkSearchParam,
|
||||
linkSn: "",
|
||||
linkNm: "",
|
||||
linkClfsCd: "ALL",
|
||||
});
|
||||
};
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, linkSearchParam],
|
||||
queryFn: () => LinkApi.getLinkList(linkSearchParam),
|
||||
const handleSubmit = async (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setLinkSearchParam({
|
||||
...linkSearchParam,
|
||||
linkSn,
|
||||
linkNm,
|
||||
linkClfsCd,
|
||||
});
|
||||
};
|
||||
|
||||
const handleResetSearchParam = () => {
|
||||
setLinkSn('');
|
||||
setLinkNm('');
|
||||
setLinkClfsCd('ALL');
|
||||
setLinkSearchParam({
|
||||
...linkSearchParam,
|
||||
linkSn: '',
|
||||
linkNm: '',
|
||||
linkClfsCd: 'ALL',
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setLinkSearchParam({
|
||||
...linkSearchParam,
|
||||
linkSn,
|
||||
linkNm,
|
||||
linkClfsCd,
|
||||
});
|
||||
};
|
||||
|
||||
const handleRowClick = (link: Link) => {
|
||||
queryClient.removeQueries({
|
||||
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`],
|
||||
});
|
||||
queryClient.removeQueries({
|
||||
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkInfo`],
|
||||
});
|
||||
router.push(`link/register/${link.link_sn}`);
|
||||
};
|
||||
|
||||
const handleClickRegist = () => {
|
||||
router.push('link/register');
|
||||
};
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
onRowClick: handleRowClick,
|
||||
viewPagination: true,
|
||||
},
|
||||
const handleRowClick = (link: Link) => {
|
||||
queryClient.removeQueries({
|
||||
queryKey: [
|
||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`,
|
||||
],
|
||||
});
|
||||
queryClient.removeQueries({
|
||||
queryKey: [
|
||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkInfo`,
|
||||
],
|
||||
});
|
||||
router.push(`link/register/${link.link_sn}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
<form className="flex items-center gap-2 p-2" onSubmit={handleSubmit}>
|
||||
<Label htmlFor="liSnSearchInput" className="mr-2 text-label">
|
||||
일련번호
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-4"
|
||||
id="liSnSearchInput"
|
||||
value={linkSn}
|
||||
onChange={(e) => setLinkSn(e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Label htmlFor="liNmSearchInput" className="mr-2 text-label">
|
||||
연계 명
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-2"
|
||||
id="liNmSearchInput"
|
||||
value={linkNm}
|
||||
onChange={(e) => setLinkNm(e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Label htmlFor="linkClfsCd" className="mr-2 text-label">
|
||||
연계 분류
|
||||
</Label>
|
||||
<Select onValueChange={(e) => setLinkClfsCd(e)} value={linkClfsCd}>
|
||||
<SelectTrigger id="linkClfsCd" className="w-[20rem] mr-4">
|
||||
<SelectValue placeholder="전체"></SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
<SelectItem value="link0001">TIBERO 데이터베이스</SelectItem>
|
||||
<SelectItem value="link0002">ORACLE 데이터베이스</SelectItem>
|
||||
<SelectItem value="link0003">공공데이터 포털 API</SelectItem>
|
||||
<SelectItem value="link0004">한강홍수통제소 API</SelectItem>
|
||||
<SelectItem value="link0005">기상청 API</SelectItem>
|
||||
<SelectItem value="link0006">국가지하수정보센터 API</SelectItem>
|
||||
<SelectItem value="link0007">행정정보공동이용센터 연계</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button color="green" className="h-[3.2rem]" type="button" onClick={handleResetSearchParam}>
|
||||
초기화
|
||||
</Button>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border flex flex-col h-[calc(100vh-28rem)]">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">연계 목록</h3>
|
||||
|
||||
<div className="flex items-center">
|
||||
<LinkExpiredAlert linkList={query} />
|
||||
<Button onClick={handleClickRegist}>등록</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] overflow-auto h-full !p-0">
|
||||
{!query.isLoading ?
|
||||
<div>{tableComponent}</div>
|
||||
:
|
||||
<div className="[data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center h-full">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
const handleClickRegist = () => {
|
||||
router.push("link/register");
|
||||
};
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
onRowClick: handleRowClick,
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
<form className="flex items-center gap-2 p-2" onSubmit={handleSubmit}>
|
||||
<Label htmlFor="liSnSearchInput" className="mr-2 text-label">
|
||||
일련번호
|
||||
</Label>
|
||||
<Input
|
||||
className="mr-4 h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
id="liSnSearchInput"
|
||||
value={linkSn}
|
||||
onChange={(e) => setLinkSn(e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Label htmlFor="liNmSearchInput" className="mr-2 text-label">
|
||||
연계 명
|
||||
</Label>
|
||||
<Input
|
||||
className="mr-2 h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
id="liNmSearchInput"
|
||||
value={linkNm}
|
||||
onChange={(e) => setLinkNm(e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Label htmlFor="linkClfsCd" className="mr-2 text-label">
|
||||
연계 분류
|
||||
</Label>
|
||||
<Select onValueChange={(e) => setLinkClfsCd(e)} value={linkClfsCd}>
|
||||
<SelectTrigger id="linkClfsCd" className="mr-4 w-[20rem]">
|
||||
<SelectValue placeholder="전체"></SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
<SelectItem value="link0001">TIBERO 데이터베이스</SelectItem>
|
||||
<SelectItem value="link0002">ORACLE 데이터베이스</SelectItem>
|
||||
<SelectItem value="link0003">공공데이터 포털 API</SelectItem>
|
||||
<SelectItem value="link0004">한강홍수통제소 API</SelectItem>
|
||||
<SelectItem value="link0005">기상청 API</SelectItem>
|
||||
<SelectItem value="link0006">국가지하수정보센터 API</SelectItem>
|
||||
<SelectItem value="link0007">
|
||||
행정정보공동이용센터 연계
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
color="green"
|
||||
className="h-[3.2rem]"
|
||||
type="button"
|
||||
onClick={handleResetSearchParam}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border flex h-[calc(100vh-28rem)] flex-col">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">연계 목록</h3>
|
||||
|
||||
<div className="flex items-center">
|
||||
<LinkExpiredAlert linkList={query} />
|
||||
<Button onClick={handleClickRegist}>등록</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
||||
{!query.isLoading ? (
|
||||
<div>{tableComponent}</div>
|
||||
) : (
|
||||
<div className="[data-theme='black']:bg-transparent flex h-full items-center justify-center rounded-lg p-10">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { LinkPage };
|
||||
export { LinkPage };
|
||||
|
||||
@@ -3,10 +3,25 @@
|
||||
import "@common/assets/styles/grid.css";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button, Input, Label, DatePicker, Select, SelectTrigger, SelectContent, SelectItem, SelectValue, LoadingWithAnimation } from "@common/components";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
DatePicker,
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
LoadingWithAnimation,
|
||||
} from "@common/components";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { getCoreRowModel, type PaginationState, type RowSelectionState, } from "@tanstack/react-table";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { LinkApi, type LinkSearchParam } from "@/entities/link";
|
||||
import { columns } from "../model/table-columns";
|
||||
import dayjs from "dayjs";
|
||||
@@ -14,176 +29,200 @@ import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
|
||||
function LinkLogPage() {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const [linkSn, setLinkSn] = useState<string>('');
|
||||
const [linkNm, setLinkNm] = useState<string>('');
|
||||
const [isSucc, setIsSucc] = useState<string>('ALL');
|
||||
const [startDate, setStartDate] = useState<string>(dayjs().subtract(7, "day").format('YYYY-MM-DD'));
|
||||
const [endDate, setEndDate] = useState<string>(dayjs().format('YYYY-MM-DD'));
|
||||
const [linkSn, setLinkSn] = useState<string>("");
|
||||
const [linkNm, setLinkNm] = useState<string>("");
|
||||
const [isSucc, setIsSucc] = useState<string>("ALL");
|
||||
const [startDate, setStartDate] = useState<string>(
|
||||
dayjs().subtract(7, "day").format("YYYY-MM-DD"),
|
||||
);
|
||||
const [endDate, setEndDate] = useState<string>(dayjs().format("YYYY-MM-DD"));
|
||||
|
||||
const [linkSearchParam, setLinkSearchParam] = useState<LinkSearchParam>({
|
||||
linkSn: '',
|
||||
linkNm: '',
|
||||
isSucc: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
const [linkSearchParam, setLinkSearchParam] = useState<LinkSearchParam>({
|
||||
linkSn: "",
|
||||
linkNm: "",
|
||||
isSucc: "",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
});
|
||||
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 100,
|
||||
});
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [
|
||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`,
|
||||
linkSearchParam,
|
||||
pagination,
|
||||
],
|
||||
queryFn: () =>
|
||||
LinkApi.getLinkLogList({
|
||||
...linkSearchParam,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
isSucc,
|
||||
startDate,
|
||||
endDate,
|
||||
}),
|
||||
});
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
const handleResetSearchParam = () => {
|
||||
setIsSucc("ALL");
|
||||
setLinkNm("");
|
||||
setLinkSn("");
|
||||
setStartDate(dayjs().subtract(7, "day").format("YYYY-MM-DD"));
|
||||
setEndDate(dayjs().format("YYYY-MM-DD"));
|
||||
setLinkSearchParam({
|
||||
...linkSearchParam,
|
||||
linkSn: "",
|
||||
linkNm: "",
|
||||
isSucc: "ALL",
|
||||
startDate: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
|
||||
endDate: dayjs().format("YYYY-MM-DD"),
|
||||
});
|
||||
};
|
||||
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 100,
|
||||
});
|
||||
const handleSubmit = async (e?: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e?.preventDefault();
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, linkSearchParam, pagination],
|
||||
queryFn: () =>
|
||||
LinkApi.getLinkLogList({
|
||||
...linkSearchParam,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
isSucc,
|
||||
startDate,
|
||||
endDate,
|
||||
}),
|
||||
});
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
const handleResetSearchParam = () => {
|
||||
setIsSucc('ALL');
|
||||
setLinkNm('');
|
||||
setLinkSn('');
|
||||
setStartDate(dayjs().subtract(7, "day").format('YYYY-MM-DD'));
|
||||
setEndDate(dayjs().format('YYYY-MM-DD'));
|
||||
setLinkSearchParam({
|
||||
...linkSearchParam,
|
||||
linkSn: '',
|
||||
linkNm: '',
|
||||
isSucc: 'ALL',
|
||||
startDate: dayjs().subtract(7, "day").format('YYYY-MM-DD'),
|
||||
endDate: dayjs().format('YYYY-MM-DD'),
|
||||
if (startDate && endDate) {
|
||||
if (startDate > endDate) {
|
||||
alert("종료일이 시작일보다 이전일 수 없습니다.");
|
||||
return;
|
||||
} else {
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
};
|
||||
setLinkSearchParam({
|
||||
...linkSearchParam,
|
||||
linkSn,
|
||||
linkNm,
|
||||
isSucc,
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
alert("조회 날짜를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e?: React.FormEvent<HTMLFormElement>) => {
|
||||
e?.preventDefault();
|
||||
|
||||
if (startDate && endDate) {
|
||||
|
||||
if (startDate > endDate) {
|
||||
alert('종료일이 시작일보다 이전일 수 없습니다.');
|
||||
return;
|
||||
} else {
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setLinkSearchParam({
|
||||
...linkSearchParam,
|
||||
linkSn,
|
||||
linkNm,
|
||||
isSucc,
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
alert('조회 날짜를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
<form className="flex items-center gap-2 p-2" onSubmit={handleSubmit}>
|
||||
<Label htmlFor="liSnSearchInput" className="mr-2 text-label">
|
||||
일련번호
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-4"
|
||||
id="liSnSearchInput"
|
||||
value={linkSn}
|
||||
onChange={(e) => setLinkSn(e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Label htmlFor="liNmSearchInput" className="mr-2 text-label">
|
||||
연계 명
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-4"
|
||||
id="liNmSearchInput"
|
||||
value={linkNm}
|
||||
onChange={(e) => setLinkNm(e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Label htmlFor="liResultCode" className="mr-2 text-label">
|
||||
실행결과
|
||||
</Label>
|
||||
<Select onValueChange={(e) => setIsSucc(e)} value={isSucc}>
|
||||
<SelectTrigger id="liResultCode" className="w-[10rem] mr-4">
|
||||
<SelectValue placeholder="전체"></SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
<SelectItem value="SUCC">성공</SelectItem>
|
||||
<SelectItem value="FAIL">실패</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Label htmlFor="sDateSearchInput" className="mx-2 text-label">
|
||||
시작일
|
||||
</Label>
|
||||
<DatePicker id="sDateSearchInput" mode={"single"} selected={dayjs(startDate).toDate()} onSelect={(e) => setStartDate(dayjs(e).format('YYYY-MM-DD'))} />
|
||||
<Label htmlFor="eDateSearchInput" className="mx-2 text-label">
|
||||
종료일
|
||||
</Label>
|
||||
<DatePicker id="eDateSearchInput" mode={"single"} className="mr-2" selected={dayjs(endDate).toDate()} onSelect={(e) => setEndDate(dayjs(e).format('YYYY-MM-DD'))} />
|
||||
<Button color="green" className="h-[3.2rem]" type="button" onClick={handleResetSearchParam}>
|
||||
초기화
|
||||
</Button>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border flex flex-col h-[calc(100vh-28rem)]">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">로그 목록 (검색 조건에 따른 건수 : {query.data?.totalElements.toLocaleString('ko-KR') ?? 0} )</h3>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] overflow-auto h-full !p-0">
|
||||
{query.isLoading ?
|
||||
<div className="[data-theme='black']:bg-transparent rounded-lg p-10 h-full flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div>{tableComponent}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
<form className="flex items-center gap-2 p-2" onSubmit={handleSubmit}>
|
||||
<Label htmlFor="liSnSearchInput" className="mr-2 text-label">
|
||||
일련번호
|
||||
</Label>
|
||||
<Input
|
||||
className="mr-4 h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
id="liSnSearchInput"
|
||||
value={linkSn}
|
||||
onChange={(e) => setLinkSn(e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Label htmlFor="liNmSearchInput" className="mr-2 text-label">
|
||||
연계 명
|
||||
</Label>
|
||||
<Input
|
||||
className="mr-4 h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
id="liNmSearchInput"
|
||||
value={linkNm}
|
||||
onChange={(e) => setLinkNm(e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Label htmlFor="liResultCode" className="mr-2 text-label">
|
||||
실행결과
|
||||
</Label>
|
||||
<Select onValueChange={(e) => setIsSucc(e)} value={isSucc}>
|
||||
<SelectTrigger id="liResultCode" className="mr-4 w-[10rem]">
|
||||
<SelectValue placeholder="전체"></SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
<SelectItem value="SUCC">성공</SelectItem>
|
||||
<SelectItem value="FAIL">실패</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Label htmlFor="sDateSearchInput" className="mx-2 text-label">
|
||||
시작일
|
||||
</Label>
|
||||
<DatePicker
|
||||
id="sDateSearchInput"
|
||||
mode={"single"}
|
||||
selected={dayjs(startDate).toDate()}
|
||||
onSelect={(e) => setStartDate(dayjs(e).format("YYYY-MM-DD"))}
|
||||
/>
|
||||
<Label htmlFor="eDateSearchInput" className="mx-2 text-label">
|
||||
종료일
|
||||
</Label>
|
||||
<DatePicker
|
||||
id="eDateSearchInput"
|
||||
mode={"single"}
|
||||
className="mr-2"
|
||||
selected={dayjs(endDate).toDate()}
|
||||
onSelect={(e) => setEndDate(dayjs(e).format("YYYY-MM-DD"))}
|
||||
/>
|
||||
<Button
|
||||
color="green"
|
||||
className="h-[3.2rem]"
|
||||
type="button"
|
||||
onClick={handleResetSearchParam}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border flex h-[calc(100vh-28rem)] flex-col">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
로그 목록 (검색 조건에 따른 건수 :{" "}
|
||||
{query.data?.totalElements.toLocaleString("ko-KR") ?? 0} )
|
||||
</h3>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
||||
{query.isLoading ? (
|
||||
<div className="[data-theme='black']:bg-transparent flex h-full items-center justify-center rounded-lg p-10">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>{tableComponent}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { LinkLogPage };
|
||||
export { LinkLogPage };
|
||||
|
||||
@@ -1,20 +1,31 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Input,
|
||||
Label,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
LoadingWithAnimation,
|
||||
Button,
|
||||
DataTable,
|
||||
Input,
|
||||
Label,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
LoadingWithAnimation,
|
||||
} from "@common/components";
|
||||
import { getCoreRowModel, useReactTable, type PaginationState, type RowSelectionState, } from "@tanstack/react-table";
|
||||
import { LinkApi, type Link, type LinkSearchParam, type LinkVrblFormData, linkVrblFormSchema } from "@/entities/link";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
LinkApi,
|
||||
type Link,
|
||||
type LinkSearchParam,
|
||||
type LinkVrblFormData,
|
||||
linkVrblFormSchema,
|
||||
} from "@/entities/link";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { columns } from "../model/table-columns";
|
||||
import { vrblColumns } from "../model/vrbl-table-columns";
|
||||
@@ -31,376 +42,388 @@ import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
|
||||
function LinkParamsPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [linkSn, setLinkSn] = useState<string>('');
|
||||
const [linkNm, setLinkNm] = useState<string>('');
|
||||
const [linkSn, setLinkSn] = useState<string>("");
|
||||
const [linkNm, setLinkNm] = useState<string>("");
|
||||
|
||||
const [linkSearchParam, setLinkSearchParam] = useState<LinkSearchParam>({
|
||||
linkSn: '',
|
||||
linkNm: ''
|
||||
const [linkSearchParam, setLinkSearchParam] = useState<LinkSearchParam>({
|
||||
linkSn: "",
|
||||
linkNm: "",
|
||||
});
|
||||
|
||||
const [selectedLink, setSelectedLink] = useState<{
|
||||
linkSn: string;
|
||||
linkNm: string;
|
||||
}>({
|
||||
linkSn: "",
|
||||
linkNm: "",
|
||||
});
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 100,
|
||||
});
|
||||
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const [selectedVrblIds, setSelectedVrblIds] = useState<string[]>([]);
|
||||
const [isDBdata, setIsDBData] = useState<boolean>(false);
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
const [onConfirm, setOnConfirm] = useState<boolean>(false);
|
||||
|
||||
const userUniqId = getCookie(AUTH_USER_ID) ?? "";
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const dbIdList = ["link0001", "link0002", "link0007"];
|
||||
|
||||
// Link 목록 쿼리
|
||||
const query = useQuery({
|
||||
queryKey: ["api/link/getLinkList", linkSearchParam],
|
||||
queryFn: () =>
|
||||
LinkApi.getLinkList({
|
||||
...linkSearchParam,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
});
|
||||
|
||||
// LinkVrbl 목록 쿼리 - 별도 관리
|
||||
const linkVrblQuery = useQuery({
|
||||
queryKey: ["linkVrblList", selectedLink.linkSn],
|
||||
queryFn: () => LinkApi.getLinkVrblList(selectedLink.linkSn),
|
||||
enabled: !!selectedLink.linkSn && !dbIdList.includes(selectedLink.linkSn),
|
||||
staleTime: 0, // 항상 최신 데이터를 가져오도록 설정
|
||||
});
|
||||
|
||||
const handleResetSearchParam = () => {
|
||||
setSelectedLink({
|
||||
linkSn: "",
|
||||
linkNm: "",
|
||||
});
|
||||
|
||||
const [selectedLink, setSelectedLink] = useState<{
|
||||
linkSn: string;
|
||||
linkNm: string;
|
||||
}>({
|
||||
linkSn: '',
|
||||
linkNm: ''
|
||||
setLinkSn("");
|
||||
setLinkNm("");
|
||||
setLinkSearchParam({
|
||||
...linkSearchParam,
|
||||
linkSn: "",
|
||||
linkNm: "",
|
||||
});
|
||||
};
|
||||
|
||||
const handleConfirm = async (callback: () => Promise<void>) => {
|
||||
setOnConfirm(true);
|
||||
await callback();
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const { mutateAsync: insertLinkVrbl } = useMutation({
|
||||
mutationFn: async (data: LinkVrblFormData) =>
|
||||
await LinkApi.insertLinkVrbl({
|
||||
...data,
|
||||
link_sn: selectedLink.linkSn,
|
||||
mdfr_id: userUniqId,
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
// 쿼리 무효화를 통한 최신 데이터 가져오기
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["linkVrblList", selectedLink.linkSn],
|
||||
});
|
||||
setDialogOpen(false);
|
||||
alert("저장되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: updateLinkVrbl } = useMutation({
|
||||
mutationFn: async (data: LinkVrblFormData) =>
|
||||
await LinkApi.updateLinkVrblInfo({
|
||||
...data,
|
||||
link_sn: selectedLink.linkSn,
|
||||
mdfr_id: userUniqId,
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["linkVrblList", selectedLink.linkSn],
|
||||
});
|
||||
setDialogOpen(false);
|
||||
alert("저장되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteLinkVrbl } = useMutation({
|
||||
mutationFn: async (data: string[]) => await LinkApi.deleteLinkVrbl(data),
|
||||
onSuccess: async () => {
|
||||
await queryClient.refetchQueries({
|
||||
queryKey: ["linkVrblList", selectedLink.linkSn],
|
||||
});
|
||||
|
||||
setSelectedVrblIds([]);
|
||||
alert("삭제되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
// linkVrblQuery의 데이터를 사용
|
||||
const tableData = useMemo(() => {
|
||||
return linkVrblQuery.data?.content ?? [];
|
||||
}, [linkVrblQuery.data]);
|
||||
|
||||
const vrblCols = useMemo(
|
||||
() =>
|
||||
vrblColumns({
|
||||
selectedVrblIds,
|
||||
setSelectedVrblIds,
|
||||
}),
|
||||
[selectedVrblIds],
|
||||
);
|
||||
|
||||
const vrblTable = useReactTable({
|
||||
data: tableData,
|
||||
columns: vrblCols,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const form = useForm<LinkVrblFormData>({
|
||||
resolver: zodResolver(linkVrblFormSchema),
|
||||
defaultValues: {},
|
||||
});
|
||||
|
||||
const handleRowClick = async (link: Link) => {
|
||||
setSelectedLink({
|
||||
linkSn: link.link_sn,
|
||||
linkNm: link.link_nm,
|
||||
});
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
setSelectedVrblIds([]);
|
||||
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 100,
|
||||
if (dbIdList.includes(link.link_clsf_cd)) {
|
||||
setIsDBData(true);
|
||||
} else {
|
||||
setIsDBData(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
setSelectedLink({
|
||||
linkSn: "",
|
||||
linkNm: "",
|
||||
});
|
||||
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const [selectedVrblIds, setSelectedVrblIds] = useState<string[]>([]);
|
||||
const [isDBdata, setIsDBData] = useState<boolean>(false);
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
const [onConfirm, setOnConfirm] = useState<boolean>(false);
|
||||
|
||||
const userUniqId = getCookie(AUTH_USER_ID) ?? "";
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const dbIdList = ['link0001', 'link0002', 'link0007'];
|
||||
|
||||
// Link 목록 쿼리
|
||||
const query = useQuery({
|
||||
queryKey: ['api/link/getLinkList', linkSearchParam],
|
||||
queryFn: () => LinkApi.getLinkList({
|
||||
...linkSearchParam,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
setLinkSearchParam({
|
||||
...linkSearchParam,
|
||||
linkSn,
|
||||
linkNm,
|
||||
});
|
||||
};
|
||||
|
||||
// LinkVrbl 목록 쿼리 - 별도 관리
|
||||
const linkVrblQuery = useQuery({
|
||||
queryKey: ['linkVrblList', selectedLink.linkSn],
|
||||
queryFn: () => LinkApi.getLinkVrblList(selectedLink.linkSn),
|
||||
enabled: !!selectedLink.linkSn && !dbIdList.includes(selectedLink.linkSn),
|
||||
staleTime: 0, // 항상 최신 데이터를 가져오도록 설정
|
||||
});
|
||||
const fetchUserInfo = async () => {
|
||||
const userData = await getUserInfo();
|
||||
return userData.userName;
|
||||
};
|
||||
|
||||
const handleResetSearchParam = () => {
|
||||
setSelectedLink({
|
||||
linkSn: '',
|
||||
linkNm: ''
|
||||
});
|
||||
|
||||
setLinkSn('');
|
||||
setLinkNm('');
|
||||
setLinkSearchParam({
|
||||
...linkSearchParam,
|
||||
linkSn: '',
|
||||
linkNm: ''
|
||||
});
|
||||
};
|
||||
|
||||
const handleConfirm = async (callback: () => Promise<void>) => {
|
||||
setOnConfirm(true);
|
||||
await callback();
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const { mutateAsync: insertLinkVrbl } = useMutation({
|
||||
mutationFn: async (data: LinkVrblFormData) =>
|
||||
await LinkApi.insertLinkVrbl({
|
||||
...data,
|
||||
link_sn: selectedLink.linkSn,
|
||||
mdfr_id: userUniqId,
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
// 쿼리 무효화를 통한 최신 데이터 가져오기
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['linkVrblList', selectedLink.linkSn]
|
||||
});
|
||||
setDialogOpen(false);
|
||||
alert('저장되었습니다.');
|
||||
const handleDeleteParam = async () => {
|
||||
if (selectedVrblIds.length === 0) {
|
||||
alert("삭제할 파라미터를 선택해 주세요.");
|
||||
} else {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("선택한 파라미터 정보를 삭제하시겠습니까?")) {
|
||||
deleteLinkVrbl(selectedVrblIds);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { mutateAsync: updateLinkVrbl } = useMutation({
|
||||
mutationFn: async (data: LinkVrblFormData) => await LinkApi.updateLinkVrblInfo({
|
||||
...data,
|
||||
link_sn: selectedLink.linkSn,
|
||||
mdfr_id: userUniqId,
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['linkVrblList', selectedLink.linkSn]
|
||||
});
|
||||
setDialogOpen(false);
|
||||
alert('저장되었습니다.');
|
||||
}
|
||||
});
|
||||
const handleVrblRowClick = async (data: LinkVrbl) => {
|
||||
const linkVrblInfo = await LinkApi.getLinkVrblInfo(data.link_vrbl_sn);
|
||||
|
||||
const { mutateAsync: deleteLinkVrbl } = useMutation({
|
||||
mutationFn: async (data: string[]) => await LinkApi.deleteLinkVrbl(data),
|
||||
onSuccess: async () => {
|
||||
await queryClient.refetchQueries({
|
||||
queryKey: ['linkVrblList', selectedLink.linkSn]
|
||||
});
|
||||
|
||||
setSelectedVrblIds([]);
|
||||
alert('삭제되었습니다.');
|
||||
}
|
||||
});
|
||||
if (linkVrblInfo) {
|
||||
let newData = { ...linkVrblInfo };
|
||||
|
||||
// linkVrblQuery의 데이터를 사용
|
||||
const tableData = useMemo(() => {
|
||||
return linkVrblQuery.data?.content ?? [];
|
||||
}, [linkVrblQuery.data]);
|
||||
if (linkVrblInfo.mdfr_id) {
|
||||
const userName = await fetchUserInfo();
|
||||
newData = {
|
||||
...linkVrblInfo,
|
||||
mdfr_id: userName,
|
||||
};
|
||||
}
|
||||
|
||||
const vrblCols = useMemo(
|
||||
() =>
|
||||
vrblColumns({
|
||||
selectedVrblIds,
|
||||
setSelectedVrblIds,
|
||||
}),
|
||||
[selectedVrblIds]
|
||||
);
|
||||
form.reset(newData);
|
||||
}
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const vrblTable = useReactTable({
|
||||
data: tableData,
|
||||
columns: vrblCols,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
onRowClick: handleRowClick,
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<LinkVrblFormData>({
|
||||
resolver: zodResolver(linkVrblFormSchema),
|
||||
defaultValues: {},
|
||||
});
|
||||
// LinkVrbl 데이터 표시 로직
|
||||
const renderLinkVrblContent = () => {
|
||||
if (isDBdata) {
|
||||
return (
|
||||
<div className="mt-14 flex flex-1 items-center justify-center text-3xl text-gray-500">
|
||||
변수를 지원하지 않는 연계 데이터입니다.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleRowClick = async (link: Link) => {
|
||||
setSelectedLink({
|
||||
linkSn: link.link_sn,
|
||||
linkNm: link.link_nm,
|
||||
});
|
||||
if (!selectedLink.linkSn) {
|
||||
return (
|
||||
<div className="mt-14 flex flex-1 items-center justify-center text-3xl text-gray-500">
|
||||
연계 데이터를 선택해주세요.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
setSelectedVrblIds([]);
|
||||
|
||||
if (dbIdList.includes(link.link_clsf_cd)) {
|
||||
setIsDBData(true);
|
||||
} else {
|
||||
setIsDBData(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
setSelectedLink({
|
||||
linkSn: '',
|
||||
linkNm: ''
|
||||
});
|
||||
|
||||
setLinkSearchParam({
|
||||
...linkSearchParam,
|
||||
linkSn,
|
||||
linkNm
|
||||
});
|
||||
};
|
||||
|
||||
const fetchUserInfo = async () => {
|
||||
const userData = await getUserInfo();
|
||||
return userData.userName;
|
||||
};
|
||||
|
||||
const handleDeleteParam = async () => {
|
||||
if (selectedVrblIds.length === 0) {
|
||||
alert('삭제할 파라미터를 선택해 주세요.');
|
||||
} else {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm('선택한 파라미터 정보를 삭제하시겠습니까?')) {
|
||||
deleteLinkVrbl(selectedVrblIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleVrblRowClick = async (data: LinkVrbl) => {
|
||||
const linkVrblInfo = await LinkApi.getLinkVrblInfo(data.link_vrbl_sn);
|
||||
|
||||
if (linkVrblInfo) {
|
||||
let newData = { ...linkVrblInfo };
|
||||
|
||||
if (linkVrblInfo.mdfr_id) {
|
||||
const userName = await fetchUserInfo();
|
||||
newData = {
|
||||
...linkVrblInfo,
|
||||
mdfr_id: userName,
|
||||
};
|
||||
}
|
||||
|
||||
form.reset(newData);
|
||||
}
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
onRowClick: handleRowClick,
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
// LinkVrbl 데이터 표시 로직
|
||||
const renderLinkVrblContent = () => {
|
||||
if (isDBdata) {
|
||||
return (
|
||||
<div className="flex items-center justify-center mt-14 text-gray-500 text-3xl flex-1">
|
||||
변수를 지원하지 않는 연계 데이터입니다.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!selectedLink.linkSn) {
|
||||
return (
|
||||
<div className="flex items-center justify-center mt-14 text-gray-500 text-3xl flex-1">
|
||||
연계 데이터를 선택해주세요.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (linkVrblQuery.isLoading) {
|
||||
return (
|
||||
<div className="[data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center h-full">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2 mt-14 flex justify-end items-center flex-none">
|
||||
{tableData.length > 0 && (
|
||||
<Button color="red" className="h-[3.2rem] mr-2" type="button" onClick={handleDeleteParam}>
|
||||
행 삭제
|
||||
</Button>
|
||||
)}
|
||||
<LinkParamsDialog
|
||||
selectedLink={selectedLink}
|
||||
form={form}
|
||||
insertLinkVrbl={insertLinkVrbl}
|
||||
dialogOpen={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
updateLinkVrbl={updateLinkVrbl}
|
||||
/>
|
||||
</div>
|
||||
{tableData.length > 0 ? (
|
||||
<div className="overflow-auto flex-1 min-h-[300px]">
|
||||
<DataTable table={vrblTable} onRowClick={handleVrblRowClick} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-20 text-gray-500 text-3xl">
|
||||
등록된 연계 변수가 없습니다.
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
if (linkVrblQuery.isLoading) {
|
||||
return (
|
||||
<div className="[data-theme='black']:bg-transparent flex h-full items-center justify-center rounded-lg p-10">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
<form className="flex items-center gap-2 p-2" onSubmit={handleSubmit}>
|
||||
<Label htmlFor="liSnSearchInput" className="mr-2 text-label">
|
||||
일련번호
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-4"
|
||||
id="liSnSearchInput"
|
||||
value={linkSn}
|
||||
onChange={(e) => setLinkSn(e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Label htmlFor="liNmSearchInput" className="mr-2 text-label">
|
||||
연계 명
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-2"
|
||||
id="liNmSearchInput"
|
||||
value={linkNm}
|
||||
onChange={(e) => setLinkNm(e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Button color="green" className="h-[3.2rem]" type="button" onClick={handleResetSearchParam}>
|
||||
초기화
|
||||
</Button>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card !mb-0 flex h-[calc(100vh-28rem)] gap-5 !p-0">
|
||||
<div className="card card-border w-1/2 !mx-0 flex flex-col">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">연계 목록</h3>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] overflow-auto !p-0 h-full">
|
||||
{!query.isLoading ? (
|
||||
<div>{tableComponent}</div>
|
||||
) : (
|
||||
<div className="[data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center h-full">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card-border w-1/2 !mx-0 flex flex-col">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">연계 변수 정보</h3>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0 overflow-auto h-full flex flex-col">
|
||||
<Table variant="secondary" className="flex-none">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>연계 일련번호</TableHead>
|
||||
<TableCell>
|
||||
{selectedLink.linkSn ? selectedLink.linkSn : '-'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>연계 명</TableHead>
|
||||
<TableCell>
|
||||
{selectedLink.linkNm ? selectedLink.linkNm : '-'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
{renderLinkVrblContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmDialog isOpen={onConfirm} />
|
||||
<>
|
||||
<div className="mb-2 mt-14 flex flex-none items-center justify-end">
|
||||
{tableData.length > 0 && (
|
||||
<Button
|
||||
color="red"
|
||||
className="mr-2 h-[3.2rem]"
|
||||
type="button"
|
||||
onClick={handleDeleteParam}
|
||||
>
|
||||
행 삭제
|
||||
</Button>
|
||||
)}
|
||||
<LinkParamsDialog
|
||||
selectedLink={selectedLink}
|
||||
form={form}
|
||||
insertLinkVrbl={insertLinkVrbl}
|
||||
dialogOpen={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
updateLinkVrbl={updateLinkVrbl}
|
||||
/>
|
||||
</div>
|
||||
{tableData.length > 0 ? (
|
||||
<div className="min-h-[300px] flex-1 overflow-auto">
|
||||
<DataTable table={vrblTable} onRowClick={handleVrblRowClick} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-20 items-center justify-center text-3xl text-gray-500">
|
||||
등록된 연계 변수가 없습니다.
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
<form className="flex items-center gap-2 p-2" onSubmit={handleSubmit}>
|
||||
<Label htmlFor="liSnSearchInput" className="mr-2 text-label">
|
||||
일련번호
|
||||
</Label>
|
||||
<Input
|
||||
className="mr-4 h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
id="liSnSearchInput"
|
||||
value={linkSn}
|
||||
onChange={(e) => setLinkSn(e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Label htmlFor="liNmSearchInput" className="mr-2 text-label">
|
||||
연계 명
|
||||
</Label>
|
||||
<Input
|
||||
className="mr-2 h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
id="liNmSearchInput"
|
||||
value={linkNm}
|
||||
onChange={(e) => setLinkNm(e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Button
|
||||
color="green"
|
||||
className="h-[3.2rem]"
|
||||
type="button"
|
||||
onClick={handleResetSearchParam}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card !mb-0 flex h-[calc(100vh-28rem)] gap-5 !p-0">
|
||||
<div className="card card-border !mx-0 flex w-1/2 flex-col">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">연계 목록</h3>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
||||
{!query.isLoading ? (
|
||||
<div>{tableComponent}</div>
|
||||
) : (
|
||||
<div className="[data-theme='black']:bg-transparent flex h-full items-center justify-center rounded-lg p-10">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card-border !mx-0 flex w-1/2 flex-col">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">연계 변수 정보</h3>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] flex h-full flex-col overflow-auto !p-0">
|
||||
<Table variant="secondary" className="flex-none">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>연계 일련번호</TableHead>
|
||||
<TableCell>
|
||||
{selectedLink.linkSn ? selectedLink.linkSn : "-"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>연계 명</TableHead>
|
||||
<TableCell>
|
||||
{selectedLink.linkNm ? selectedLink.linkNm : "-"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
{renderLinkVrblContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmDialog isOpen={onConfirm} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { LinkParamsPage };
|
||||
export { LinkParamsPage };
|
||||
|
||||
@@ -3,376 +3,429 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import { LinkApi, type LinkStats, type LinkSearchParam } from "@/entities/link";
|
||||
import { Button, Label, DatePicker, LoadingWithAnimation } from "@common/components";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Tooltip,
|
||||
Legend,
|
||||
type ChartOptions,
|
||||
type Chart,
|
||||
Button,
|
||||
Label,
|
||||
DatePicker,
|
||||
LoadingWithAnimation,
|
||||
} from "@common/components";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Tooltip,
|
||||
Legend,
|
||||
type ChartOptions,
|
||||
type Chart,
|
||||
} from "chart.js";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import { getCoreRowModel, type PaginationState, type RowSelectionState, } from "@tanstack/react-table";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { columns } from "@/pages/linkLog/model/table-columns";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
interface LinkStatsListObject {
|
||||
linkSuccStats: LinkStats[] | null;
|
||||
linkFailStats: LinkStats[] | null;
|
||||
linkSuccStats: LinkStats[] | null;
|
||||
linkFailStats: LinkStats[] | null;
|
||||
}
|
||||
|
||||
function LinkStatsPage() {
|
||||
const succChartRef = useRef<Chart<"line"> | null>(null);
|
||||
const failChartRef = useRef<Chart<"line"> | null>(null);
|
||||
|
||||
const succChartRef = useRef<Chart<"line"> | null>(null);
|
||||
const failChartRef = useRef<Chart<"line"> | null>(null);
|
||||
const [startDate, setStartDate] = useState<string>(
|
||||
dayjs().subtract(7, "day").format("YYYY-MM-DD"),
|
||||
);
|
||||
const [endDate, setEndDate] = useState<string>(dayjs().format("YYYY-MM-DD"));
|
||||
|
||||
const [startDate, setStartDate] = useState<string>(dayjs().subtract(7, "day").format('YYYY-MM-DD'));
|
||||
const [endDate, setEndDate] = useState<string>(dayjs().format('YYYY-MM-DD'));
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 100,
|
||||
});
|
||||
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 100,
|
||||
const [linkStatsList, setLinkStatsList] =
|
||||
useState<LinkStatsListObject | null>(null);
|
||||
const [logQueryParam, setLogQueryParam] = useState<{
|
||||
isSucc: "SUCC" | "FAIL";
|
||||
linkSn?: string;
|
||||
linkNm?: string;
|
||||
} | null>(null);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [
|
||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`,
|
||||
logQueryParam,
|
||||
pagination,
|
||||
],
|
||||
queryFn: () =>
|
||||
LinkApi.getLinkLogList({
|
||||
...logQueryParam,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
startDate,
|
||||
endDate,
|
||||
}),
|
||||
enabled: !!logQueryParam,
|
||||
});
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
const [linkSearchParam, setLinkSearchParam] = useState<LinkSearchParam>({
|
||||
linkSn: "",
|
||||
linkNm: "",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
});
|
||||
|
||||
ChartJS.register(
|
||||
LineElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
Tooltip,
|
||||
Legend,
|
||||
);
|
||||
|
||||
const succData = {
|
||||
labels: linkStatsList?.linkSuccStats?.map((e) => e.link_nm),
|
||||
datasets: [
|
||||
{
|
||||
label: "연계 성공",
|
||||
data: linkStatsList?.linkSuccStats?.map((e) => e.count),
|
||||
fill: false,
|
||||
borderColor: "rgb(53, 162, 235)",
|
||||
backgroundColor: "rgb(53, 162, 235)",
|
||||
tension: 0.1,
|
||||
pointRadius: 6,
|
||||
pointHoverRadius: 8,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const succOptions: ChartOptions<"line"> = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "top",
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const failData = {
|
||||
labels: linkStatsList?.linkFailStats?.map((e) => e.link_nm),
|
||||
datasets: [
|
||||
{
|
||||
label: "연계 실패",
|
||||
data: linkStatsList?.linkFailStats?.map((e) => e.count),
|
||||
fill: false,
|
||||
borderColor: "rgb(255, 99, 132)",
|
||||
backgroundColor: "rgb(255, 99, 132)",
|
||||
tension: 0.1,
|
||||
pointRadius: 6,
|
||||
pointHoverRadius: 8,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const failOptions: ChartOptions<"line"> = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "top",
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const handleResetSearchParam = async () => {
|
||||
setLinkSearchParam({
|
||||
linkSn: "",
|
||||
linkNm: "",
|
||||
startDate: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
|
||||
endDate: dayjs().format("YYYY-MM-DD"),
|
||||
});
|
||||
|
||||
const [linkStatsList, setLinkStatsList] = useState<LinkStatsListObject | null>(null);
|
||||
const [logQueryParam, setLogQueryParam] = useState<{
|
||||
isSucc: 'SUCC' | 'FAIL';
|
||||
linkSn?: string;
|
||||
linkNm?: string;
|
||||
} | null>(null);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, logQueryParam, pagination],
|
||||
queryFn: () =>
|
||||
LinkApi.getLinkLogList({
|
||||
...logQueryParam,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
startDate,
|
||||
endDate,
|
||||
}),
|
||||
enabled: !!logQueryParam,
|
||||
setLogQueryParam({
|
||||
isSucc: "SUCC",
|
||||
linkNm: "",
|
||||
linkSn: "",
|
||||
});
|
||||
|
||||
setStartDate(dayjs().subtract(7, "day").format("YYYY-MM-DD"));
|
||||
setEndDate(dayjs().format("YYYY-MM-DD"));
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
viewPagination: true,
|
||||
},
|
||||
const data = await LinkApi.getLinkStatsList({
|
||||
...linkSearchParam,
|
||||
linkSn: "",
|
||||
linkNm: "",
|
||||
startDate: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
|
||||
endDate: dayjs().format("YYYY-MM-DD"),
|
||||
});
|
||||
setLinkStatsList(data);
|
||||
};
|
||||
|
||||
const [linkSearchParam, setLinkSearchParam] = useState<LinkSearchParam>({
|
||||
linkSn: '',
|
||||
linkNm: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
});
|
||||
const handleSubmit = async (e?: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e?.preventDefault();
|
||||
|
||||
ChartJS.register(
|
||||
LineElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
Tooltip,
|
||||
Legend
|
||||
if (startDate && endDate) {
|
||||
if (startDate > endDate) {
|
||||
alert("종료일이 시작일보다 이전일 수 없습니다.");
|
||||
return;
|
||||
} else {
|
||||
const data = await LinkApi.getLinkStatsList({
|
||||
...linkSearchParam,
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
setLinkStatsList(data);
|
||||
setLogQueryParam({
|
||||
isSucc: "SUCC",
|
||||
linkNm: "",
|
||||
linkSn: "",
|
||||
});
|
||||
setLinkSearchParam({
|
||||
...linkSearchParam,
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
alert("조회 날짜를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSuccChartClick = async (
|
||||
event: React.MouseEvent<HTMLCanvasElement>,
|
||||
) => {
|
||||
if (!succChartRef.current) return;
|
||||
|
||||
const chart = succChartRef.current;
|
||||
|
||||
const points = chart.getElementsAtEventForMode(
|
||||
event.nativeEvent,
|
||||
"nearest",
|
||||
{ intersect: true },
|
||||
false,
|
||||
);
|
||||
|
||||
const succData = {
|
||||
labels: linkStatsList?.linkSuccStats?.map((e) => e.link_nm),
|
||||
datasets: [
|
||||
{
|
||||
label: '연계 성공',
|
||||
data: linkStatsList?.linkSuccStats?.map((e) => e.count),
|
||||
fill: false,
|
||||
borderColor: 'rgb(53, 162, 235)',
|
||||
backgroundColor: 'rgb(53, 162, 235)',
|
||||
tension: 0.1,
|
||||
pointRadius: 6,
|
||||
pointHoverRadius: 8,
|
||||
},
|
||||
],
|
||||
if (points[0] && linkStatsList?.linkSuccStats) {
|
||||
const index = points[0].index;
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
pageSize: 100,
|
||||
});
|
||||
setLogQueryParam({
|
||||
isSucc: "SUCC",
|
||||
linkNm: linkStatsList.linkSuccStats[index]?.link_nm,
|
||||
linkSn: linkStatsList.linkSuccStats[index]?.link_sn,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleFailChartClick = async (
|
||||
event: React.MouseEvent<HTMLCanvasElement>,
|
||||
) => {
|
||||
if (!failChartRef.current) return;
|
||||
|
||||
const chart = failChartRef.current;
|
||||
|
||||
const points = chart.getElementsAtEventForMode(
|
||||
event.nativeEvent,
|
||||
"nearest",
|
||||
{ intersect: true },
|
||||
false,
|
||||
);
|
||||
|
||||
if (points[0] && linkStatsList?.linkFailStats) {
|
||||
const index = points[0].index;
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
pageSize: 100,
|
||||
});
|
||||
setLogQueryParam({
|
||||
isSucc: "FAIL",
|
||||
linkNm: linkStatsList.linkFailStats[index]?.link_nm,
|
||||
linkSn: linkStatsList.linkFailStats[index]?.link_sn,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitToday = async () => {
|
||||
const data = await LinkApi.getLinkStatsList({
|
||||
...linkSearchParam,
|
||||
startDate: dayjs().format("YYYY-MM-DD"),
|
||||
endDate: dayjs().format("YYYY-MM-DD"),
|
||||
});
|
||||
setEndDate(dayjs().format("YYYY-MM-DD"));
|
||||
setStartDate(dayjs().format("YYYY-MM-DD"));
|
||||
setLinkStatsList(data);
|
||||
setLinkSearchParam({
|
||||
...linkSearchParam,
|
||||
startDate: dayjs().format("YYYY-MM-DD"),
|
||||
endDate: dayjs().format("YYYY-MM-DD"),
|
||||
});
|
||||
setLogQueryParam({
|
||||
isSucc: "SUCC",
|
||||
linkNm: "",
|
||||
linkSn: "",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const data = await LinkApi.getLinkStatsList({
|
||||
...linkSearchParam,
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
setLinkStatsList(data);
|
||||
};
|
||||
|
||||
const succOptions: ChartOptions<'line'> = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
setLinkSearchParam({
|
||||
...linkSearchParam,
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
|
||||
const failData = {
|
||||
labels: linkStatsList?.linkFailStats?.map((e) => e.link_nm),
|
||||
datasets: [
|
||||
{
|
||||
label: '연계 실패',
|
||||
data: linkStatsList?.linkFailStats?.map((e) => e.count),
|
||||
fill: false,
|
||||
borderColor: 'rgb(255, 99, 132)',
|
||||
backgroundColor: 'rgb(255, 99, 132)',
|
||||
tension: 0.1,
|
||||
pointRadius: 6,
|
||||
pointHoverRadius: 8,
|
||||
},
|
||||
],
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const failOptions: ChartOptions<'line'> = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const handleResetSearchParam = async () => {
|
||||
setLinkSearchParam({
|
||||
linkSn: '',
|
||||
linkNm: '',
|
||||
startDate: dayjs().subtract(7, "day").format('YYYY-MM-DD'),
|
||||
endDate: dayjs().format('YYYY-MM-DD'),
|
||||
});
|
||||
|
||||
setLogQueryParam({
|
||||
isSucc: 'SUCC',
|
||||
linkNm: '',
|
||||
linkSn: '',
|
||||
});
|
||||
|
||||
setStartDate(dayjs().subtract(7, "day").format('YYYY-MM-DD'));
|
||||
setEndDate(dayjs().format('YYYY-MM-DD'));
|
||||
|
||||
const data = await LinkApi.getLinkStatsList({
|
||||
...linkSearchParam,
|
||||
linkSn: '',
|
||||
linkNm: '',
|
||||
startDate: dayjs().subtract(7, "day").format('YYYY-MM-DD'),
|
||||
endDate: dayjs().format('YYYY-MM-DD'),
|
||||
});
|
||||
setLinkStatsList(data);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e?: React.FormEvent<HTMLFormElement>) => {
|
||||
e?.preventDefault();
|
||||
|
||||
if (startDate && endDate) {
|
||||
|
||||
if (startDate > endDate) {
|
||||
alert('종료일이 시작일보다 이전일 수 없습니다.');
|
||||
return;
|
||||
} else {
|
||||
const data = await LinkApi.getLinkStatsList({
|
||||
...linkSearchParam,
|
||||
startDate,
|
||||
endDate
|
||||
});
|
||||
setLinkStatsList(data);
|
||||
setLogQueryParam({
|
||||
isSucc: 'SUCC',
|
||||
linkNm: '',
|
||||
linkSn: '',
|
||||
});
|
||||
setLinkSearchParam({
|
||||
...linkSearchParam,
|
||||
startDate,
|
||||
endDate
|
||||
});
|
||||
}
|
||||
} else {
|
||||
alert('조회 날짜를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSuccChartClick = async (event: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
if (!succChartRef.current) return;
|
||||
|
||||
const chart = succChartRef.current;
|
||||
|
||||
const points = chart.getElementsAtEventForMode(
|
||||
event.nativeEvent,
|
||||
"nearest",
|
||||
{ intersect: true },
|
||||
false
|
||||
);
|
||||
|
||||
if (points[0] && linkStatsList?.linkSuccStats) {
|
||||
const index = points[0].index;
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
pageSize: 100,
|
||||
});
|
||||
setLogQueryParam({
|
||||
isSucc: 'SUCC',
|
||||
linkNm: linkStatsList.linkSuccStats[index]?.link_nm,
|
||||
linkSn: linkStatsList.linkSuccStats[index]?.link_sn,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleFailChartClick = async (event: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
if (!failChartRef.current) return;
|
||||
|
||||
const chart = failChartRef.current;
|
||||
|
||||
const points = chart.getElementsAtEventForMode(
|
||||
event.nativeEvent,
|
||||
"nearest",
|
||||
{ intersect: true },
|
||||
false
|
||||
);
|
||||
|
||||
if (points[0] && linkStatsList?.linkFailStats) {
|
||||
const index = points[0].index;
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
pageSize: 100,
|
||||
});
|
||||
setLogQueryParam({
|
||||
isSucc: 'FAIL',
|
||||
linkNm: linkStatsList.linkFailStats[index]?.link_nm,
|
||||
linkSn: linkStatsList.linkFailStats[index]?.link_sn,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitToday = async () => {
|
||||
const data = await LinkApi.getLinkStatsList({
|
||||
...linkSearchParam,
|
||||
startDate: dayjs().format('YYYY-MM-DD'),
|
||||
endDate: dayjs().format('YYYY-MM-DD'),
|
||||
});
|
||||
setEndDate(dayjs().format('YYYY-MM-DD'));
|
||||
setStartDate(dayjs().format('YYYY-MM-DD'));
|
||||
setLinkStatsList(data);
|
||||
setLinkSearchParam({
|
||||
...linkSearchParam,
|
||||
startDate: dayjs().format('YYYY-MM-DD'),
|
||||
endDate: dayjs().format('YYYY-MM-DD'),
|
||||
});
|
||||
setLogQueryParam({
|
||||
isSucc: 'SUCC',
|
||||
linkNm: '',
|
||||
linkSn: '',
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const data = await LinkApi.getLinkStatsList({
|
||||
...linkSearchParam,
|
||||
startDate,
|
||||
endDate
|
||||
});
|
||||
setLinkStatsList(data);
|
||||
};
|
||||
|
||||
setLinkSearchParam({
|
||||
...linkSearchParam,
|
||||
startDate,
|
||||
endDate
|
||||
});
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="card !mb-0 !min-h-[calc(100vh-16.8rem)]">
|
||||
<div className="card card-border">
|
||||
<form className="flex items-center gap-2 p-2" onSubmit={handleSubmit}>
|
||||
<Label htmlFor="sDateSearchInput" className="mx-2 text-label">
|
||||
시작일
|
||||
</Label>
|
||||
<DatePicker id="sDateSearchInput" mode={"single"} selected={dayjs(startDate).toDate()} onSelect={(e) => setStartDate(dayjs(e).format('YYYY-MM-DD'))} />
|
||||
<Label htmlFor="eDateSearchInput" className="mx-2 text-label">
|
||||
종료일
|
||||
</Label>
|
||||
<DatePicker id="eDateSearchInput" mode={"single"} className="mr-2" selected={dayjs(endDate).toDate()} onSelect={(e) => setEndDate(dayjs(e).format('YYYY-MM-DD'))} />
|
||||
<Button color="green" className="h-[3.2rem]" type="button" onClick={handleResetSearchParam}>
|
||||
초기화
|
||||
</Button>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
<Button className="h-[3.2rem]" type="button" onClick={handleSubmitToday}>
|
||||
오늘 날짜로 조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card !m-[1.2rem] !p-0 flex flex-col gap-y-8">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">조회 일자 : {`${linkSearchParam.startDate} ~ ${linkSearchParam.endDate}`}</h3>
|
||||
</div>
|
||||
<div className="h-[32rem] w-full">
|
||||
<Line ref={succChartRef} data={succData} options={succOptions} onClick={handleSuccChartClick} />
|
||||
</div>
|
||||
<div className="h-[32rem]">
|
||||
<Line ref={failChartRef} data={failData} options={failOptions} onClick={handleFailChartClick} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
{logQueryParam?.linkNm ? `${logQueryParam.linkNm} 연계 ${logQueryParam.isSucc === 'FAIL' ? '실패' : '성공'} 로그 목록 (${linkSearchParam.startDate} ~ ${linkSearchParam.endDate}) - ${query.data?.totalElements.toLocaleString('ko-KR') ?? 0}건` : '로그 목록'}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-[40rem] overflow-auto !p-0">
|
||||
{logQueryParam?.linkNm ?
|
||||
query.isLoading ?
|
||||
<div className="[data-theme='black']:bg-transparent rounded-lg p-10 h-full flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div>{tableComponent}</div>
|
||||
:
|
||||
<div className="flex items-center justify-center h-full text-gray-500 text-3xl">
|
||||
차트 데이터를 선택해주세요.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div className="card !mb-0 !min-h-[calc(100vh-16.8rem)]">
|
||||
<div className="card card-border">
|
||||
<form className="flex items-center gap-2 p-2" onSubmit={handleSubmit}>
|
||||
<Label htmlFor="sDateSearchInput" className="mx-2 text-label">
|
||||
시작일
|
||||
</Label>
|
||||
<DatePicker
|
||||
id="sDateSearchInput"
|
||||
mode={"single"}
|
||||
selected={dayjs(startDate).toDate()}
|
||||
onSelect={(e) => setStartDate(dayjs(e).format("YYYY-MM-DD"))}
|
||||
/>
|
||||
<Label htmlFor="eDateSearchInput" className="mx-2 text-label">
|
||||
종료일
|
||||
</Label>
|
||||
<DatePicker
|
||||
id="eDateSearchInput"
|
||||
mode={"single"}
|
||||
className="mr-2"
|
||||
selected={dayjs(endDate).toDate()}
|
||||
onSelect={(e) => setEndDate(dayjs(e).format("YYYY-MM-DD"))}
|
||||
/>
|
||||
<Button
|
||||
color="green"
|
||||
className="h-[3.2rem]"
|
||||
type="button"
|
||||
onClick={handleResetSearchParam}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
className="h-[3.2rem]"
|
||||
type="button"
|
||||
onClick={handleSubmitToday}
|
||||
>
|
||||
오늘 날짜로 조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card !m-[1.2rem] flex flex-col gap-y-8 !p-0">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
조회 일자 :{" "}
|
||||
{`${linkSearchParam.startDate} ~ ${linkSearchParam.endDate}`}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="h-[32rem] w-full">
|
||||
<Line
|
||||
ref={succChartRef}
|
||||
data={succData}
|
||||
options={succOptions}
|
||||
onClick={handleSuccChartClick}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-[32rem]">
|
||||
<Line
|
||||
ref={failChartRef}
|
||||
data={failData}
|
||||
options={failOptions}
|
||||
onClick={handleFailChartClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
<div className="card !m-[1.2rem] !p-0">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
{logQueryParam?.linkNm
|
||||
? `${logQueryParam.linkNm} 연계 ${logQueryParam.isSucc === "FAIL" ? "실패" : "성공"} 로그 목록 (${linkSearchParam.startDate} ~ ${linkSearchParam.endDate}) - ${query.data?.totalElements.toLocaleString("ko-KR") ?? 0}건`
|
||||
: "로그 목록"}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-[40rem] overflow-auto !p-0">
|
||||
{logQueryParam?.linkNm ? (
|
||||
query.isLoading ? (
|
||||
<div className="[data-theme='black']:bg-transparent flex h-full items-center justify-center rounded-lg p-10">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>{tableComponent}</div>
|
||||
)
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-3xl text-gray-500">
|
||||
차트 데이터를 선택해주세요.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { LinkStatsPage };
|
||||
export { LinkStatsPage };
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -77,7 +77,7 @@ function LoginlogPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsManualLoading(true);
|
||||
setPagination({
|
||||
|
||||
@@ -91,7 +91,7 @@ function MenuHistoryDialog({ menuCd }: MenuHistoryDialogProps) {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSearch = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
query.refetch();
|
||||
|
||||
@@ -59,7 +59,7 @@ function OrganizationSearchDialog({
|
||||
handleUserSelect(node.data as TreeUser);
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setTree(filterUserOrganizationTreeDataByKeyword(treeData ?? [], keyword));
|
||||
|
||||
@@ -62,7 +62,7 @@ function ModulePage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { NtftalkHistoryPage } from "./ui/ntftalk-history-page";
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { NtftalkMmtLogHistory } from "@/entities/ntftalk";
|
||||
|
||||
function formatDateTime(value: string | null | undefined): string {
|
||||
if (!value) return "—";
|
||||
const normalized = value.includes("T")
|
||||
? value.replace("T", " ").slice(0, 19)
|
||||
: value.slice(0, 19);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function formatResult(successYn: string | null | undefined): string {
|
||||
if (!successYn) return "—";
|
||||
if (successYn === "SUCCESS") return "성공";
|
||||
if (successYn === "FAIL") return "실패";
|
||||
return successYn;
|
||||
}
|
||||
|
||||
export function createNtftalkHistoryColumns(): ColumnDef<NtftalkMmtLogHistory>[] {
|
||||
return [
|
||||
{
|
||||
id: "rowNo",
|
||||
header: "순번",
|
||||
enableSorting: false,
|
||||
cell: ({ row, table }) => {
|
||||
const { pageIndex, pageSize } = table.getState().pagination;
|
||||
return pageIndex * pageSize + row.index + 1;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "siteSystem",
|
||||
header: "시스템",
|
||||
cell: ({ getValue }) => (getValue() as string) || "—",
|
||||
},
|
||||
{
|
||||
accessorKey: "content",
|
||||
header: "알림내용",
|
||||
cell: ({ getValue }) => {
|
||||
const v = getValue() as string;
|
||||
if (!v) return "—";
|
||||
return (
|
||||
<span className="line-clamp-2 max-w-[28rem] text-left" title={v}>
|
||||
{v}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "recipientNum",
|
||||
header: "수신자",
|
||||
cell: ({ getValue }) => (getValue() as string) || "—",
|
||||
},
|
||||
{
|
||||
accessorKey: "dateRslt",
|
||||
header: "수신일",
|
||||
cell: ({ getValue }) => formatDateTime(getValue() as string | null),
|
||||
},
|
||||
{
|
||||
accessorKey: "successYn",
|
||||
header: "결과",
|
||||
cell: ({ getValue }) => formatResult(getValue() as string | null),
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useDataTable } from "@/shared/lib";
|
||||
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
||||
import {
|
||||
NtftalkMmtLogHistoryApi,
|
||||
type NtftalkMmtLogHistorySearchParams,
|
||||
} from "@/entities/ntftalk";
|
||||
import { SiteSelect } from "@/entities/site";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
|
||||
import { createNtftalkHistoryColumns } from "../model/ntftalk-history-table-columns";
|
||||
|
||||
type DateRange = {
|
||||
from?: Date | null;
|
||||
to?: Date | null;
|
||||
};
|
||||
|
||||
type PresetPeriod = "" | "3M" | "6M" | "1Y" | "CUSTOM";
|
||||
|
||||
function computeDateRangeFromPreset(
|
||||
preset: Extract<PresetPeriod, "3M" | "6M" | "1Y">,
|
||||
): DateRange {
|
||||
const to = dayjs().endOf("day").toDate();
|
||||
const base = dayjs();
|
||||
let fromDay: dayjs.Dayjs;
|
||||
switch (preset) {
|
||||
case "3M":
|
||||
fromDay = base.subtract(3, "month");
|
||||
break;
|
||||
case "6M":
|
||||
fromDay = base.subtract(6, "month");
|
||||
break;
|
||||
case "1Y":
|
||||
fromDay = base.subtract(1, "year");
|
||||
break;
|
||||
}
|
||||
return { from: fromDay.startOf("day").toDate(), to };
|
||||
}
|
||||
|
||||
function isDateRangeOverOneYear(range: DateRange): boolean {
|
||||
if (!range.from) return false;
|
||||
const oneYearAgo = dayjs().subtract(1, "year").startOf("day");
|
||||
return dayjs(range.from).isBefore(oneYearAgo);
|
||||
}
|
||||
|
||||
function NtftalkHistoryPage() {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [contentInput, setContentInput] = useState("");
|
||||
const [recipientInput, setRecipientInput] = useState("");
|
||||
const [presetPeriod, setPresetPeriod] = useState<PresetPeriod>("");
|
||||
const [dateRange, setDateRange] = useState<DateRange>({
|
||||
from: null,
|
||||
to: null,
|
||||
});
|
||||
const [resultFilter, setResultFilter] = useState("ALL");
|
||||
const [systemFilter, setSystemFilter] = useState("ALL");
|
||||
|
||||
const [searchParams, setSearchParams] =
|
||||
useState<NtftalkMmtLogHistorySearchParams>({ sort: "dateMtReport,desc" });
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const listQuery = useQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.NTFTALK_SERVICE.MMT_LOG_HISTORY,
|
||||
searchParams,
|
||||
pagination,
|
||||
],
|
||||
queryFn: () =>
|
||||
NtftalkMmtLogHistoryApi.getNtftalkMmtLogHistoryPage({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const columns = useMemo(() => createNtftalkHistoryColumns(), []);
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query: listQuery,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: { pagination },
|
||||
manualPagination: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
},
|
||||
dataTableOptions: {
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
const buildSearchParamsFromInputs = (): NtftalkMmtLogHistorySearchParams => ({
|
||||
sort: searchParams.sort ?? "dateMtReport,desc",
|
||||
content: contentInput.trim() || undefined,
|
||||
recipientNum: recipientInput.trim() || undefined,
|
||||
successYn: resultFilter,
|
||||
system: systemFilter,
|
||||
dateFrom: dateRange.from
|
||||
? dayjs(dateRange.from).format("YYYY-MM-DD")
|
||||
: undefined,
|
||||
dateTo: dateRange.to ? dayjs(dateRange.to).format("YYYY-MM-DD") : undefined,
|
||||
});
|
||||
|
||||
const handleSearch = (e: React.SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
if (isDateRangeOverOneYear(dateRange)) {
|
||||
alert("조회 기간은 오늘 기준 최대 1년까지만 선택할 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
||||
setSearchParams(buildSearchParamsFromInputs());
|
||||
};
|
||||
|
||||
const handlePresetPeriodChange = (value: PresetPeriod) => {
|
||||
setPresetPeriod(value);
|
||||
if (value === "3M" || value === "6M" || value === "1Y") {
|
||||
setDateRange(computeDateRangeFromPreset(value));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDateRangeChange = (next: DateRange) => {
|
||||
setDateRange(next);
|
||||
setPresetPeriod("CUSTOM");
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setContentInput("");
|
||||
setRecipientInput("");
|
||||
setPresetPeriod("");
|
||||
setDateRange({ from: null, to: null });
|
||||
setResultFilter("ALL");
|
||||
setSystemFilter("ALL");
|
||||
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
||||
setSearchParams({ sort: "dateMtReport,desc" });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6 flex flex-col gap-4">
|
||||
<div className="card card-border">
|
||||
<form
|
||||
onSubmit={handleSearch}
|
||||
className="flex flex-col gap-4 p-2 md:flex-row md:items-end md:justify-between"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Label className="whitespace-nowrap text-label">조회기간</Label>
|
||||
<Select
|
||||
value={presetPeriod}
|
||||
onValueChange={handlePresetPeriodChange}
|
||||
>
|
||||
<SelectTrigger className="w-[16rem]">
|
||||
<SelectValue placeholder="기간 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
<SelectItem value="6M">6개월</SelectItem>
|
||||
<SelectItem value="1Y">1년</SelectItem>
|
||||
<SelectItem value="CUSTOM">직접입력</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputDateRangePicker
|
||||
dateRange={dateRange}
|
||||
setDateRange={handleDateRangeChange}
|
||||
disabled={presetPeriod !== "CUSTOM"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="whitespace-nowrap text-label">알림내용</Label>
|
||||
<Input
|
||||
className="w-[16rem]"
|
||||
value={contentInput}
|
||||
onChange={(e) => setContentInput(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="whitespace-nowrap text-label">사용자</Label>
|
||||
<Input
|
||||
className="w-[16rem]"
|
||||
value={recipientInput}
|
||||
onChange={(e) => setRecipientInput(e.target.value)}
|
||||
placeholder="수신자 번호"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="whitespace-nowrap text-label">결과</Label>
|
||||
<Select value={resultFilter} onValueChange={setResultFilter}>
|
||||
<SelectTrigger className="w-[16rem]">
|
||||
<SelectValue placeholder="전체" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
<SelectItem value="SUCCESS">성공</SelectItem>
|
||||
<SelectItem value="FAIL">실패</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="whitespace-nowrap text-label">시스템</Label>
|
||||
<SiteSelect
|
||||
className="w-[16rem]"
|
||||
value={systemFilter}
|
||||
onValueChange={(value) => setSystemFilter(value)}
|
||||
enabledAllSite
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 gap-2">
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
className="h-[3.2rem]"
|
||||
onClick={handleReset}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">알림톡 발송 이력</h3>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { NtftalkHistoryPage };
|
||||
@@ -1,6 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { NoData } from "@/shared/ui/no-data";
|
||||
import { NtftalkTempApi } from "@/entities/ntftalk";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -13,9 +17,6 @@ import {
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { NtftalkTempApi } from "@/entities/ntftalk";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { NoData } from "@/shared/ui/no-data";
|
||||
|
||||
interface NtftalkTempDetailDialogProps {
|
||||
templateCode: string | null;
|
||||
|
||||
@@ -2,12 +2,38 @@
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useFieldArray, useForm, useWatch } from "react-hook-form";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
keepPreviousData,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AUTH_USER_ID, ENDPOINT } from "@/shared/config";
|
||||
import {
|
||||
getCookie,
|
||||
useAlertStore,
|
||||
useDataTable,
|
||||
useMutationWrapper,
|
||||
} from "@/shared/lib";
|
||||
import { NoData } from "@/shared/ui/no-data";
|
||||
import {
|
||||
NtftalkBiztalkApi,
|
||||
type NtftalkBiztalkInspectionRequestItem,
|
||||
type NtftalkTemp,
|
||||
NtftalkTempApi,
|
||||
type NtftalkTempSearchParams,
|
||||
NtftalkVarApi,
|
||||
type NtftalkVarSearchParams,
|
||||
} from "@/entities/ntftalk";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
@@ -20,36 +46,14 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
NtftalkBiztalkApi,
|
||||
NtftalkTempApi,
|
||||
NtftalkVarApi,
|
||||
type NtftalkBiztalkInspectionRequestItem,
|
||||
type NtftalkTemp,
|
||||
type NtftalkTempSearchParams,
|
||||
type NtftalkVarSearchParams,
|
||||
} from "@/entities/ntftalk";
|
||||
import { AUTH_USER_ID, ENDPOINT } from "@/shared/config";
|
||||
import {
|
||||
getCookie,
|
||||
useAlertStore,
|
||||
useDataTable,
|
||||
useMutationWrapper,
|
||||
} from "@/shared/lib";
|
||||
import { NoData } from "@/shared/ui/no-data";
|
||||
import { createTemplateColumns } from "../model/template-table-columns";
|
||||
|
||||
import {
|
||||
createEmptyVarDraft,
|
||||
type NtftalkVarGridRow,
|
||||
} from "../model/ntftalk-var-grid";
|
||||
import { createTemplateColumns } from "../model/template-table-columns";
|
||||
import { createVarColumns } from "../model/var-table-columns";
|
||||
|
||||
import { NtftalkTempDetailDialog } from "./ntftalk-temp-detail-dialog";
|
||||
import { NtftalkTempRegistDialog } from "./ntftalk-temp-regist-dialog";
|
||||
|
||||
@@ -250,7 +254,7 @@ function NtftalkTempPage() {
|
||||
messageTypeFilter === ALL ? undefined : messageTypeFilter,
|
||||
});
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
const handleSearch = (e: React.SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
||||
setSearchParams(buildSearchParamsFromInputs());
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
keepPreviousData,
|
||||
useQuery,
|
||||
@@ -13,10 +14,28 @@ import {
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { NoData } from "@/shared/ui/no-data";
|
||||
import {
|
||||
NtftalkBiztalkApi,
|
||||
NtftalkTempApi,
|
||||
NtftalkVarApi,
|
||||
type NtftalkVarSearchParams,
|
||||
} from "@/entities/ntftalk";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
@@ -33,29 +52,13 @@ import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
Textarea,
|
||||
TableRow,
|
||||
DialogFooter,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import {
|
||||
NtftalkBiztalkApi,
|
||||
NtftalkTempApi,
|
||||
NtftalkVarApi,
|
||||
type NtftalkVarSearchParams,
|
||||
} from "@/entities/ntftalk";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
|
||||
import { NoData } from "@/shared/ui/no-data";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import {
|
||||
ntftalkTempSaveFormSchema,
|
||||
type NtftalkTempSaveFormData,
|
||||
ntftalkTempSaveFormSchema,
|
||||
} from "../model/ntftalk-temp-save-schema";
|
||||
import { createVarColumns } from "../model/var-table-columns";
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ function PopupPage() {
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
|
||||
@@ -77,7 +77,7 @@ function PrivacyLogPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
|
||||
@@ -127,7 +127,7 @@ function RecentPage() {
|
||||
|
||||
const isSelectedRow = table.getFilteredSelectedRowModel().rows.length > 0;
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDeletePostData(delYn === "Y");
|
||||
setPagination({
|
||||
|
||||
@@ -83,7 +83,7 @@ function MappingRoleDialog({
|
||||
);
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const keyword = formData.get("keyword") as string;
|
||||
|
||||
@@ -64,7 +64,7 @@ function RolePage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
|
||||
@@ -35,7 +35,7 @@ function SitePage() {
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
|
||||
@@ -71,7 +71,7 @@ function SurveyPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
|
||||
@@ -37,7 +37,7 @@ function TemplatePage() {
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
|
||||
@@ -38,7 +38,7 @@ function TermsPage() {
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
|
||||
@@ -148,7 +148,7 @@ function UserStatisticsPage() {
|
||||
setPresetDateRange("");
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const params: ConnectionStatisticsParams = {
|
||||
keywordType,
|
||||
|
||||
@@ -102,7 +102,7 @@ function UsersPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
|
||||
@@ -70,6 +70,7 @@ export const ENDPOINT = {
|
||||
NTFTALK_VAR: "/ntftalk-service/api/v1/ntftalk-var",
|
||||
NTFTALK_TEMP: "/ntftalk-service/api/v1/ntftalk-temp",
|
||||
NTFTALK_BIZTALK: "/ntftalk-service/api/v1/ntftalk-biztalk",
|
||||
MMT_LOG_HISTORY: "/ntftalk-service/api/v1/ata/mmt-log-history",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import { InputDatePicker } from "@common/components";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { InputDatePicker } from "@common/components";
|
||||
|
||||
import { useAlertStore } from "../lib";
|
||||
|
||||
type DateRange = {
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
from?: Date | null;
|
||||
to?: Date | null;
|
||||
};
|
||||
|
||||
type InputDateRangePickerProps = {
|
||||
dateRange: DateRange;
|
||||
setDateRange: (dateRange: DateRange) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
function InputDateRangePicker({
|
||||
dateRange,
|
||||
setDateRange,
|
||||
disabled,
|
||||
}: InputDateRangePickerProps) {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
@@ -32,6 +36,7 @@ function InputDateRangePicker({
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, from: date });
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
~
|
||||
<InputDatePicker
|
||||
@@ -39,6 +44,7 @@ function InputDateRangePicker({
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, to: date });
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -135,7 +135,7 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { LoginApi } from "@/entities/auth";
|
||||
import "./login.css";
|
||||
|
||||
function LoginPage() {
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = async (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
|
||||
@@ -39,8 +39,10 @@ type TreeNode = {
|
||||
function CategoryPage() {
|
||||
const router = useRouter();
|
||||
const [treeData, setTreeData] = useState<NodeModel<TreeNode>[]>([]);
|
||||
const [selectedParentNode, setSelectedParentNode] = useState<NodeModel<TreeNode> | null>(null);
|
||||
const [selectedSubNode, setSelectedSubNode] = useState<NodeModel<TreeNode> | null>(null);
|
||||
const [selectedParentNode, setSelectedParentNode] =
|
||||
useState<NodeModel<TreeNode> | null>(null);
|
||||
const [selectedSubNode, setSelectedSubNode] =
|
||||
useState<NodeModel<TreeNode> | null>(null);
|
||||
const [categoryQuery, setSearchText] = useState<CategoryParams>({
|
||||
clsfSystmNm: "",
|
||||
});
|
||||
@@ -48,7 +50,9 @@ function CategoryPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { setMessage: showAlert } = useAlertStore((state) => state);
|
||||
const { confirm } = useConfirm();
|
||||
const [dialog, setDialog] = useState<null | { type: "add" | "edit" | "delete" }>(null);
|
||||
const [dialog, setDialog] = useState<null | {
|
||||
type: "add" | "edit" | "delete";
|
||||
}>(null);
|
||||
const userUnqId = useUserStore((state) => state.userInfo?.userUnqId || "");
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const [expandedIds, setExpandedIds] = useState<(string | number)[]>([]);
|
||||
@@ -57,7 +61,8 @@ function CategoryPage() {
|
||||
const categoryNameInputRef = useRef<HTMLInputElement>(null);
|
||||
const [showCategoryNameError, setShowCategoryNameError] = useState(false);
|
||||
const [isAddingMode, setIsAddingMode] = useState(false);
|
||||
const [addingParentNode, setAddingParentNode] = useState<NodeModel<TreeNode> | null>(null);
|
||||
const [addingParentNode, setAddingParentNode] =
|
||||
useState<NodeModel<TreeNode> | null>(null);
|
||||
|
||||
const { data: categoryList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.DC_SERVICE.CATEGORY],
|
||||
@@ -70,20 +75,18 @@ function CategoryPage() {
|
||||
refetch();
|
||||
}, [refetch]);
|
||||
|
||||
|
||||
|
||||
const handleDrop = (newTreeData: NodeModel<TreeNode>[]) => {
|
||||
setTreeData(newTreeData);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const treeElement = document.querySelector('.tree-container');
|
||||
const treeElement = document.querySelector(".tree-container");
|
||||
if (treeElement) {
|
||||
expandedIds.forEach(id => {
|
||||
expandedIds.forEach((id) => {
|
||||
const nodeElement = treeElement.querySelector(`[data-id="${id}"]`);
|
||||
if (nodeElement) {
|
||||
const toggleButton = nodeElement.querySelector('.toggle-button');
|
||||
if (toggleButton && !nodeElement.classList.contains('expanded')) {
|
||||
const toggleButton = nodeElement.querySelector(".toggle-button");
|
||||
if (toggleButton && !nodeElement.classList.contains("expanded")) {
|
||||
(toggleButton as HTMLElement).click();
|
||||
}
|
||||
}
|
||||
@@ -91,7 +94,7 @@ function CategoryPage() {
|
||||
}
|
||||
}, [expandedIds]);
|
||||
|
||||
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSearch = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!searchTerm.trim()) {
|
||||
showAlert("검색어를 입력해주세요");
|
||||
@@ -99,7 +102,9 @@ function CategoryPage() {
|
||||
}
|
||||
|
||||
const searchResults = treeData.filter((node) =>
|
||||
(node.text ?? "").toLowerCase().includes((searchTerm ?? "").toLowerCase())
|
||||
(node.text ?? "")
|
||||
.toLowerCase()
|
||||
.includes((searchTerm ?? "").toLowerCase()),
|
||||
);
|
||||
|
||||
if (searchResults.length === 0) {
|
||||
@@ -138,7 +143,7 @@ function CategoryPage() {
|
||||
setNewCategoryName("");
|
||||
} else {
|
||||
setSelectedSubNode(node);
|
||||
const parentNode = treeData.find(n => n.id === node.parent) || null;
|
||||
const parentNode = treeData.find((n) => n.id === node.parent) || null;
|
||||
setSelectedParentNode(parentNode);
|
||||
setNewCategoryName(node.text || "");
|
||||
|
||||
@@ -147,7 +152,7 @@ function CategoryPage() {
|
||||
const newExpandedIds = new Set(expandedIds);
|
||||
while (parentId && parentId !== 0 && parentId !== "0") {
|
||||
newExpandedIds.add(parentId);
|
||||
const parentNode = treeData.find(n => n.id === parentId);
|
||||
const parentNode = treeData.find((n) => n.id === parentId);
|
||||
if (parentNode) {
|
||||
parentId = parentNode.parent;
|
||||
} else {
|
||||
@@ -230,7 +235,7 @@ function CategoryPage() {
|
||||
showAlert("수정할 분류 구조를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await CategoryApi.updateCategory({
|
||||
@@ -283,7 +288,7 @@ function CategoryPage() {
|
||||
showAlert("상위 분류를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await CategoryApi.createCategory({
|
||||
@@ -362,7 +367,7 @@ function CategoryPage() {
|
||||
// 백엔드에서 트리 구조로 반환하므로 flat 구조로 변환
|
||||
const flattenTree = (nodes: any[], parentId = "0"): any[] => {
|
||||
const result: any[] = [];
|
||||
|
||||
|
||||
nodes.forEach((node) => {
|
||||
// 현재 노드를 flat 구조로 추가
|
||||
result.push({
|
||||
@@ -371,65 +376,67 @@ function CategoryPage() {
|
||||
droppable: node.droppAble,
|
||||
text: node.clsfSystmNm,
|
||||
});
|
||||
|
||||
|
||||
// 자식 노드들이 있으면 재귀적으로 처리
|
||||
if (node.children && node.children.length > 0) {
|
||||
result.push(...flattenTree(node.children, node.clsfSystmSn));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
const mappedData = flattenTree(categoryList);
|
||||
|
||||
|
||||
// 트리 데이터를 영문순서, 한글순서로 정렬 (각 레벨별로)
|
||||
const sortTreeData = (nodes: typeof mappedData): typeof mappedData => {
|
||||
// 부모별로 그룹화
|
||||
const groupedByParent = new Map<string, typeof mappedData>();
|
||||
|
||||
nodes.forEach(node => {
|
||||
|
||||
nodes.forEach((node) => {
|
||||
const parent = node.parent;
|
||||
if (!groupedByParent.has(parent)) {
|
||||
groupedByParent.set(parent, []);
|
||||
}
|
||||
groupedByParent.get(parent)!.push(node);
|
||||
});
|
||||
|
||||
|
||||
// 각 그룹 내에서 정렬 (최상위 부모부터)
|
||||
const sortedGroups = Array.from(groupedByParent.entries()).sort(([parentA], [parentB]) => {
|
||||
// 최상위 부모("0")를 먼저 정렬
|
||||
if (parentA === "0" && parentB !== "0") return -1;
|
||||
if (parentA !== "0" && parentB === "0") return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
const sortedGroups = Array.from(groupedByParent.entries()).sort(
|
||||
([parentA], [parentB]) => {
|
||||
// 최상위 부모("0")를 먼저 정렬
|
||||
if (parentA === "0" && parentB !== "0") return -1;
|
||||
if (parentA !== "0" && parentB === "0") return 1;
|
||||
return 0;
|
||||
},
|
||||
);
|
||||
|
||||
sortedGroups.forEach(([parent, children]) => {
|
||||
children.sort((a, b) => {
|
||||
const textA = a.text || "";
|
||||
const textB = b.text || "";
|
||||
|
||||
|
||||
// 영문이 먼저 오도록 정렬
|
||||
const isEnglishA = /^[a-zA-Z]/.test(textA);
|
||||
const isEnglishB = /^[a-zA-Z]/.test(textB);
|
||||
|
||||
|
||||
if (isEnglishA && !isEnglishB) return -1;
|
||||
if (!isEnglishA && isEnglishB) return 1;
|
||||
|
||||
|
||||
// 같은 언어 내에서는 알파벳/가나다순 정렬
|
||||
return textA.localeCompare(textB, ['ko', 'en']);
|
||||
return textA.localeCompare(textB, ["ko", "en"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// 정렬된 결과를 다시 합치기 (순서 보장)
|
||||
const sortedData: typeof mappedData = [];
|
||||
sortedGroups.forEach(([parent, children]) => {
|
||||
sortedData.push(...children);
|
||||
});
|
||||
|
||||
|
||||
return sortedData;
|
||||
};
|
||||
|
||||
|
||||
// 각 레벨별로 정렬
|
||||
const sortedData = sortTreeData(mappedData);
|
||||
setTreeData(sortedData);
|
||||
@@ -443,8 +450,12 @@ function CategoryPage() {
|
||||
|
||||
const filterTree = (nodes: NodeModel<TreeNode>[]) => {
|
||||
return nodes.reduce((acc: NodeModel<TreeNode>[], node) => {
|
||||
const nodeMatches = (node.text ?? "").toLowerCase().includes((searchTerm ?? "").toLowerCase());
|
||||
const filteredChildren = node.droppable ? filterTree(treeData.filter(n => n.parent === node.id)) : [];
|
||||
const nodeMatches = (node.text ?? "")
|
||||
.toLowerCase()
|
||||
.includes((searchTerm ?? "").toLowerCase());
|
||||
const filteredChildren = node.droppable
|
||||
? filterTree(treeData.filter((n) => n.parent === node.id))
|
||||
: [];
|
||||
|
||||
if (nodeMatches || filteredChildren.length > 0) {
|
||||
acc.push(node);
|
||||
@@ -466,14 +477,16 @@ function CategoryPage() {
|
||||
setAddingParentNode(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
}, [userInfo, userUnqId]);
|
||||
useEffect(() => {}, [userInfo, userUnqId]);
|
||||
|
||||
return (
|
||||
<PageLayout pageTitle="자료관 분류 관리">
|
||||
<div className="card !mb-0 flex flex-col !min-h-[calc(100vh-16.8rem)]">
|
||||
<div className="card !mb-0 flex !min-h-[calc(100vh-16.8rem)] flex-col">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSearch} className="flex items-center gap-2 p-2 mb-2">
|
||||
<form
|
||||
onSubmit={handleSearch}
|
||||
className="mb-2 flex items-center gap-2 p-2"
|
||||
>
|
||||
<label htmlFor="category-search" className="mr-2 text-label">
|
||||
분류명
|
||||
</label>
|
||||
@@ -484,50 +497,55 @@ function CategoryPage() {
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
<Button className="h-[3.6rem] rounded-sm text-[1.3rem]" type="submit">
|
||||
<Button
|
||||
className="h-[3.6rem] rounded-sm text-[1.3rem]"
|
||||
type="submit"
|
||||
>
|
||||
검색
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="h-[3.6rem] !bg-gray-600 hover:!bg-gray-600 flex items-center gap-2"
|
||||
<Button
|
||||
type="button"
|
||||
className="flex h-[3.6rem] items-center gap-2 !bg-gray-600 hover:!bg-gray-600"
|
||||
onClick={handleReset}
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
초기화
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="flex flex-1 gap-2 min-h-0">
|
||||
<div className="card card-border w-[60%] flex flex-col">
|
||||
<div className="flex min-h-0 flex-1 gap-2">
|
||||
<div className="card card-border flex w-[60%] flex-col">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">자료관 분류 트리</h3>
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
자료관 분류 트리
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
className="p-4 flex-1 min-h-0 max-h-[60vh] overflow-y-auto tree-container"
|
||||
<div
|
||||
className="tree-container max-h-[60vh] min-h-0 flex-1 overflow-y-auto p-4"
|
||||
onDragOver={(e) => {
|
||||
// 파일 드래그는 방지하되, 트리 내부 드래그는 허용
|
||||
if (e.dataTransfer.types.includes('Files')) {
|
||||
if (e.dataTransfer.types.includes("Files")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
// 파일 드롭은 방지하되, 트리 내부 드롭은 허용
|
||||
if (e.dataTransfer.types.includes('Files')) {
|
||||
if (e.dataTransfer.types.includes("Files")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
onDragEnter={(e) => {
|
||||
// 파일 드래그는 방지하되, 트리 내부 드래그는 허용
|
||||
if (e.dataTransfer.types.includes('Files')) {
|
||||
if (e.dataTransfer.types.includes("Files")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
// 파일 드래그는 방지하되, 트리 내부 드래그는 허용
|
||||
if (e.dataTransfer.types.includes('Files')) {
|
||||
if (e.dataTransfer.types.includes("Files")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
@@ -543,41 +561,50 @@ function CategoryPage() {
|
||||
insertDroppableFirst={false}
|
||||
canDrop={() => false}
|
||||
render={(node, { depth, isOpen, onToggle }) => {
|
||||
const isExpanded = expandedIds.map(String).includes(String(node.id));
|
||||
const isSearchResult = searchTerm && (node.text ?? "").toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
const isExpanded = expandedIds
|
||||
.map(String)
|
||||
.includes(String(node.id));
|
||||
const isSearchResult =
|
||||
searchTerm &&
|
||||
(node.text ?? "")
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase());
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={el => {
|
||||
ref={(el) => {
|
||||
if (isSearchResult) {
|
||||
searchResultRefs.current = [...searchResultRefs.current, el];
|
||||
searchResultRefs.current = [
|
||||
...searchResultRefs.current,
|
||||
el,
|
||||
];
|
||||
}
|
||||
}}
|
||||
data-id={node.id}
|
||||
onDragOver={(e) => {
|
||||
// 파일 드래그는 방지하되, 트리 내부 드래그는 허용
|
||||
if (e.dataTransfer.types.includes('Files')) {
|
||||
if (e.dataTransfer.types.includes("Files")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
// 파일 드롭은 방지하되, 트리 내부 드롭은 허용
|
||||
if (e.dataTransfer.types.includes('Files')) {
|
||||
if (e.dataTransfer.types.includes("Files")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
onDragEnter={(e) => {
|
||||
// 파일 드래그는 방지하되, 트리 내부 드래그는 허용
|
||||
if (e.dataTransfer.types.includes('Files')) {
|
||||
if (e.dataTransfer.types.includes("Files")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
// 파일 드래그는 방지하되, 트리 내부 드래그는 허용
|
||||
if (e.dataTransfer.types.includes('Files')) {
|
||||
if (e.dataTransfer.types.includes("Files")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
@@ -586,35 +613,46 @@ function CategoryPage() {
|
||||
handleSelect(node);
|
||||
if (node.droppable) {
|
||||
onToggle();
|
||||
setExpandedIds(prev => {
|
||||
setExpandedIds((prev) => {
|
||||
const idStr = String(node.id);
|
||||
const prevStr = prev.map(String);
|
||||
const isCurrentlyExpanded = prevStr.includes(idStr);
|
||||
const isCurrentlyExpanded =
|
||||
prevStr.includes(idStr);
|
||||
if (isCurrentlyExpanded) {
|
||||
// 닫는 동작: 하위 노드에 선택된 노드가 있으면 선택 해제
|
||||
// 하위 노드 id 목록 구하기
|
||||
const getDescendantIds = (parentId: string | number): (string | number)[] => {
|
||||
const children = treeData.filter(n => n.parent === parentId);
|
||||
let all = children.map(n => n.id);
|
||||
children.forEach(child => {
|
||||
all = all.concat(getDescendantIds(child.id));
|
||||
const getDescendantIds = (
|
||||
parentId: string | number,
|
||||
): (string | number)[] => {
|
||||
const children = treeData.filter(
|
||||
(n) => n.parent === parentId,
|
||||
);
|
||||
let all = children.map((n) => n.id);
|
||||
children.forEach((child) => {
|
||||
all = all.concat(
|
||||
getDescendantIds(child.id),
|
||||
);
|
||||
});
|
||||
return all;
|
||||
};
|
||||
const descendants = getDescendantIds(node.id);
|
||||
if (
|
||||
selectedSubNode &&
|
||||
(descendants.includes(selectedSubNode.id) || selectedSubNode.id === node.id)
|
||||
(descendants.includes(selectedSubNode.id) ||
|
||||
selectedSubNode.id === node.id)
|
||||
) {
|
||||
setSelectedSubNode(null);
|
||||
setSelectedParentNode(null);
|
||||
setNewCategoryName("");
|
||||
}
|
||||
return prevStr.filter(id => id !== idStr);
|
||||
return prevStr.filter((id) => id !== idStr);
|
||||
} else {
|
||||
// 여는 동작: 자동 선택
|
||||
setSelectedSubNode(node);
|
||||
setSelectedParentNode(treeData.find(n => n.id === node.parent) || null);
|
||||
setSelectedParentNode(
|
||||
treeData.find((n) => n.id === node.parent) ||
|
||||
null,
|
||||
);
|
||||
setNewCategoryName(node.text || "");
|
||||
return [...prevStr, idStr];
|
||||
}
|
||||
@@ -622,68 +660,72 @@ function CategoryPage() {
|
||||
}
|
||||
}}
|
||||
style={{ paddingLeft: `${depth * 16}px` }}
|
||||
className={`cursor-pointer py-3 border-b border-gray-200 flex items-center transition-colors
|
||||
${String(selectedSubNode?.id) === String(node.id)
|
||||
className={`flex cursor-pointer items-center border-b border-gray-200 py-3 transition-colors ${
|
||||
String(selectedSubNode?.id) === String(node.id)
|
||||
? "bg-blue-100"
|
||||
: isSearchResult
|
||||
? "bg-sky-100"
|
||||
: "hover:bg-gray-100"}
|
||||
${isExpanded ? 'expanded' : ''}
|
||||
`}
|
||||
: "hover:bg-gray-100"
|
||||
} ${isExpanded ? "expanded" : ""} `}
|
||||
>
|
||||
{node.droppable && (
|
||||
<span
|
||||
onDragOver={(e) => {
|
||||
// 파일 드래그는 방지하되, 트리 내부 드래그는 허용
|
||||
if (e.dataTransfer.types.includes('Files')) {
|
||||
if (e.dataTransfer.types.includes("Files")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
// 파일 드롭은 방지하되, 트리 내부 드롭은 허용
|
||||
if (e.dataTransfer.types.includes('Files')) {
|
||||
if (e.dataTransfer.types.includes("Files")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
onDragEnter={(e) => {
|
||||
// 파일 드래그는 방지하되, 트리 내부 드래그는 허용
|
||||
if (e.dataTransfer.types.includes('Files')) {
|
||||
if (e.dataTransfer.types.includes("Files")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
// 파일 드래그는 방지하되, 트리 내부 드래그는 허용
|
||||
if (e.dataTransfer.types.includes('Files')) {
|
||||
if (e.dataTransfer.types.includes("Files")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
onClick={e => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle();
|
||||
setExpandedIds(prev => {
|
||||
setExpandedIds((prev) => {
|
||||
const idStr = String(node.id);
|
||||
const prevStr = prev.map(String);
|
||||
const isCurrentlyExpanded = prevStr.includes(idStr);
|
||||
const isCurrentlyExpanded =
|
||||
prevStr.includes(idStr);
|
||||
if (isCurrentlyExpanded) {
|
||||
return prevStr.filter(id => id !== idStr);
|
||||
return prevStr.filter((id) => id !== idStr);
|
||||
} else {
|
||||
setSelectedSubNode(node);
|
||||
setSelectedParentNode(treeData.find(n => n.id === node.parent) || null);
|
||||
setSelectedParentNode(
|
||||
treeData.find(
|
||||
(n) => n.id === node.parent,
|
||||
) || null,
|
||||
);
|
||||
setNewCategoryName(node.text || "");
|
||||
return [...prevStr, idStr];
|
||||
}
|
||||
});
|
||||
}}
|
||||
className="mr-2 flex items-center justify-center cursor-pointer toggle-button"
|
||||
className="toggle-button mr-2 flex cursor-pointer items-center justify-center"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-4 h-4 text-gray-500" />
|
||||
<ChevronDown className="h-4 w-4 text-gray-500" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-gray-400" />
|
||||
<ChevronRight className="h-4 w-4 text-gray-400" />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
@@ -695,36 +737,46 @@ function CategoryPage() {
|
||||
</DndProvider>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card-border flex-1 flex flex-col w-[60%] overflow-y-auto">
|
||||
<div className="card card-border flex w-[60%] flex-1 flex-col overflow-y-auto">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">분류 추가</h3>
|
||||
</div>
|
||||
<div className="p-4 flex-1 flex flex-col">
|
||||
<div className="flex flex-1 flex-col p-4">
|
||||
<div>
|
||||
<div className="mb-[10px]">
|
||||
<label className="block text-md font-medium text-label mb-1">
|
||||
<label className="text-md mb-1 block font-medium text-label">
|
||||
상위 항목
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full rounded border px-3 py-2 h-[3.2rem] !text-[1.3rem] bg-gray-200 cursor-not-allowed"
|
||||
className="h-[3.2rem] w-full cursor-not-allowed rounded border bg-gray-200 px-3 py-2 !text-[1.3rem]"
|
||||
readOnly
|
||||
placeholder="상위 분류를 선택해주세요"
|
||||
value={isAddingMode ? (addingParentNode?.text || "") : (selectedParentNode?.text || "")}
|
||||
value={
|
||||
isAddingMode
|
||||
? addingParentNode?.text || ""
|
||||
: selectedParentNode?.text || ""
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-[10px]">
|
||||
<label className="block text-md font-medium text-label mb-1 flex items-center">
|
||||
<label className="text-md mb-1 block flex items-center font-medium text-label">
|
||||
<span className="text-red-500">*</span> 분류 이름
|
||||
</label>
|
||||
<input
|
||||
ref={categoryNameInputRef}
|
||||
type="text"
|
||||
className={`w-full rounded border px-3 py-2 h-[3.2rem] !text-[1.3rem] ${
|
||||
showCategoryNameError ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||
className={`h-[3.2rem] w-full rounded border px-3 py-2 !text-[1.3rem] ${
|
||||
showCategoryNameError
|
||||
? "border-red-500 focus:border-red-500 focus:ring-red-500"
|
||||
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
}`}
|
||||
placeholder={isAddingMode ? "새로 추가할 분류 이름을 입력해주세요" : "분류 이름을 입력해주세요"}
|
||||
placeholder={
|
||||
isAddingMode
|
||||
? "새로 추가할 분류 이름을 입력해주세요"
|
||||
: "분류 이름을 입력해주세요"
|
||||
}
|
||||
value={newCategoryName}
|
||||
onChange={(e) => {
|
||||
setNewCategoryName(e.target.value);
|
||||
@@ -735,17 +787,17 @@ function CategoryPage() {
|
||||
readOnly={false}
|
||||
/>
|
||||
{showCategoryNameError && (
|
||||
<p className="text-red-600 text-sm mt-2"></p>
|
||||
<p className="mt-2 text-sm text-red-600"></p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col justify-end">
|
||||
<div className="flex justify-end mt-10 gap-2">
|
||||
<div className="flex flex-1 flex-col justify-end">
|
||||
<div className="mt-10 flex justify-end gap-2">
|
||||
{isAddingMode ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-primary text-white px-8 py-2 rounded-sm hover:bg-primary/80 text-md h-[3.2rem]"
|
||||
className="hover:bg-primary/80 text-md h-[3.2rem] rounded-sm bg-primary px-8 py-2 text-white"
|
||||
onClick={() => setDialog({ type: "add" })}
|
||||
disabled={loading}
|
||||
>
|
||||
@@ -753,7 +805,7 @@ function CategoryPage() {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-gray-500 text-white px-8 py-2 rounded-sm hover:bg-gray-600 text-md h-[3.2rem]"
|
||||
className="text-md h-[3.2rem] rounded-sm bg-gray-500 px-8 py-2 text-white hover:bg-gray-600"
|
||||
onClick={() => {
|
||||
setIsAddingMode(false);
|
||||
setAddingParentNode(null);
|
||||
@@ -771,7 +823,7 @@ function CategoryPage() {
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-primary text-white px-8 py-2 rounded-sm hover:bg-primary/80 text-md h-[3.2rem]"
|
||||
className="hover:bg-primary/80 text-md h-[3.2rem] rounded-sm bg-primary px-8 py-2 text-white"
|
||||
onClick={handleAddCategory}
|
||||
disabled={loading}
|
||||
>
|
||||
@@ -779,7 +831,7 @@ function CategoryPage() {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-gray-600 text-white px-8 py-2 rounded-sm hover:bg-gray-700 text-md h-[3.2rem]"
|
||||
className="text-md h-[3.2rem] rounded-sm bg-gray-600 px-8 py-2 text-white hover:bg-gray-700"
|
||||
onClick={handleEditCategory}
|
||||
disabled={loading}
|
||||
>
|
||||
@@ -787,7 +839,7 @@ function CategoryPage() {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-red-600 text-white px-8 py-2 rounded-sm hover:bg-red-700 text-md h-[3.2rem] cursor-pointer"
|
||||
className="text-md h-[3.2rem] cursor-pointer rounded-sm bg-red-600 px-8 py-2 text-white hover:bg-red-700"
|
||||
onClick={handleDeleteCategory}
|
||||
disabled={!selectedSubNode || loading}
|
||||
>
|
||||
@@ -807,10 +859,10 @@ function CategoryPage() {
|
||||
dialog?.type === "add"
|
||||
? "분류를 추가하시겠습니까?"
|
||||
: dialog?.type === "edit"
|
||||
? "분류를 수정하시겠습니까?"
|
||||
: dialog?.type === "delete"
|
||||
? "분류를 삭제하시겠습니까?"
|
||||
: ""
|
||||
? "분류를 수정하시겠습니까?"
|
||||
: dialog?.type === "delete"
|
||||
? "분류를 삭제하시겠습니까?"
|
||||
: ""
|
||||
}
|
||||
loading={loading}
|
||||
onConfirm={handleDialogConfirm}
|
||||
@@ -820,4 +872,4 @@ function CategoryPage() {
|
||||
);
|
||||
}
|
||||
|
||||
export { CategoryPage };
|
||||
export { CategoryPage };
|
||||
|
||||
@@ -5,12 +5,15 @@ import { Button, Input } from "@common/components";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import { UserCircle, Bot } from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { askQuestion, downloadPdf } from "@/entities/datalibrary/api/flask-service";
|
||||
import {
|
||||
askQuestion,
|
||||
downloadPdf,
|
||||
} from "@/entities/datalibrary/api/flask-service";
|
||||
|
||||
interface Message {
|
||||
role: "user" | "bot";
|
||||
text: string;
|
||||
sources?: Array<{
|
||||
sources?: Array<{
|
||||
pdf_path: string;
|
||||
original_filename: string;
|
||||
page: number;
|
||||
@@ -39,39 +42,45 @@ function ChatBotPage() {
|
||||
// RAG 요청 함수
|
||||
const handleRagRequest = async (question: string) => {
|
||||
setLoading(true);
|
||||
|
||||
|
||||
// "봇이 답변을 생성중입니다..." 메시지를 먼저 추가
|
||||
setMessages(prev => [...prev, { role: "bot", text: "봇이 답변을 생성중입니다...", sources: [] }]);
|
||||
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{ role: "bot", text: "봇이 답변을 생성중입니다...", sources: [] },
|
||||
]);
|
||||
|
||||
try {
|
||||
const data = await askQuestion(question, sessionId);
|
||||
const answer = data.answer || data.response || '답변을 찾을 수 없습니다.';
|
||||
const answer = data.answer || data.response || "답변을 찾을 수 없습니다.";
|
||||
const sources = data.sources || [];
|
||||
|
||||
|
||||
// session_id가 응답에 포함되어 있으면 저장
|
||||
if (data.session_id) {
|
||||
setSessionId(data.session_id);
|
||||
}
|
||||
|
||||
|
||||
// 답변을 한 글자씩 실시간으로 표시
|
||||
for (let i = 0; i <= answer.length; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 30)); // 30ms 딜레이
|
||||
setMessages(prev => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 30)); // 30ms 딜레이
|
||||
setMessages((prev) => {
|
||||
const newMessages = [...prev];
|
||||
// 마지막 메시지(봇 메시지)를 실제 답변으로 교체
|
||||
newMessages[newMessages.length - 1] = {
|
||||
role: "bot",
|
||||
text: answer.substring(0, i),
|
||||
sources: sources // 모든 출처 표시
|
||||
newMessages[newMessages.length - 1] = {
|
||||
role: "bot",
|
||||
text: answer.substring(0, i),
|
||||
sources: sources, // 모든 출처 표시
|
||||
};
|
||||
return newMessages;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('RAG 요청 오류:', error);
|
||||
setMessages(prev => {
|
||||
console.error("RAG 요청 오류:", error);
|
||||
setMessages((prev) => {
|
||||
const newMessages = [...prev];
|
||||
newMessages[newMessages.length - 1] = { role: "bot", text: '질문에 대한 답변을 생성하는 중 오류가 발생했습니다.' };
|
||||
newMessages[newMessages.length - 1] = {
|
||||
role: "bot",
|
||||
text: "질문에 대한 답변을 생성하는 중 오류가 발생했습니다.",
|
||||
};
|
||||
return newMessages;
|
||||
});
|
||||
} finally {
|
||||
@@ -84,7 +93,7 @@ function ChatBotPage() {
|
||||
try {
|
||||
const blob = await downloadPdf(pdfPath, fileName);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
@@ -92,87 +101,98 @@ function ChatBotPage() {
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('파일 다운로드 오류:', error);
|
||||
alert('파일 다운로드 중 오류가 발생했습니다.');
|
||||
console.error("파일 다운로드 오류:", error);
|
||||
alert("파일 다운로드 중 오류가 발생했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!input.trim() || loading) return;
|
||||
|
||||
|
||||
const question = input.trim();
|
||||
setMessages(prev => [...prev, { role: "user", text: question }]);
|
||||
setMessages((prev) => [...prev, { role: "user", text: question }]);
|
||||
setInput("");
|
||||
|
||||
|
||||
// RAG 요청
|
||||
handleRagRequest(question);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout pageTitle="하수도 GPT">
|
||||
<div className="card card-border mt-6 flex flex-col h-[calc(100vh-16.8rem)]">
|
||||
<div className="card card-border mt-6 flex h-[calc(100vh-16.8rem)] flex-col">
|
||||
<div className="card card-header flex-shrink-0">
|
||||
<h3 className="text-2xl font-medium text-label">하수도 GPT</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-3 min-h-0">
|
||||
<div className="min-h-0 flex-1 space-y-3 overflow-y-auto p-4">
|
||||
{messages.map((msg, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex items-end gap-2 max-w-[75%] ${
|
||||
className={`flex max-w-[75%] items-end gap-2 ${
|
||||
msg.role === "user"
|
||||
? "flex-row-reverse self-end ml-auto"
|
||||
: "self-start mr-auto"
|
||||
? "ml-auto flex-row-reverse self-end"
|
||||
: "mr-auto self-start"
|
||||
}`}
|
||||
>
|
||||
<span className="shrink-0 flex items-center">
|
||||
<span className="flex shrink-0 items-center">
|
||||
{msg.role === "user" ? (
|
||||
<UserCircle className="w-8 h-8 text-blue-400" />
|
||||
<UserCircle className="h-8 w-8 text-blue-400" />
|
||||
) : (
|
||||
<Bot className="w-8 h-8 text-gray-400" />
|
||||
<Bot className="h-8 w-8 text-gray-400" />
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className={`p-3 rounded-md text-2xl whitespace-pre-wrap ${
|
||||
msg.role === "user"
|
||||
? "bg-blue-100"
|
||||
: "bg-gray-100"
|
||||
className={`whitespace-pre-wrap rounded-md p-3 text-2xl ${
|
||||
msg.role === "user" ? "bg-blue-100" : "bg-gray-100"
|
||||
}`}
|
||||
style={{ maxWidth: "calc(100% - 2.5rem)" }}
|
||||
>
|
||||
{msg.text}
|
||||
{/* 출처 표시: bot 답변에만 */}
|
||||
{msg.role === "bot" && msg.sources && msg.sources.length > 0 && (
|
||||
<div className="mt-2 text-lg text-gray-500 space-y-1">
|
||||
<div className="font-semibold flex items-center gap-1">
|
||||
<span role="img" aria-label="출처">🔗</span> 출처
|
||||
{msg.role === "bot" &&
|
||||
msg.sources &&
|
||||
msg.sources.length > 0 && (
|
||||
<div className="mt-2 space-y-1 text-lg text-gray-500">
|
||||
<div className="flex items-center gap-1 font-semibold">
|
||||
<span role="img" aria-label="출처">
|
||||
🔗
|
||||
</span>{" "}
|
||||
출처
|
||||
</div>
|
||||
<ul className="ml-2 list-inside list-disc">
|
||||
{msg.sources.map((src, i) => (
|
||||
<li key={i}>
|
||||
<button
|
||||
onClick={() =>
|
||||
handleSourceDownload(
|
||||
src.pdf_path,
|
||||
src.original_filename,
|
||||
)
|
||||
}
|
||||
className="text-left underline hover:text-blue-600"
|
||||
>
|
||||
{src.original_filename} (페이지 {src.page})
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<ul className="list-disc list-inside ml-2">
|
||||
{msg.sources.map((src, i) => (
|
||||
<li key={i}>
|
||||
<button
|
||||
onClick={() => handleSourceDownload(src.pdf_path, src.original_filename)}
|
||||
className="underline hover:text-blue-600 text-left"
|
||||
>
|
||||
{src.original_filename} (페이지 {src.page})
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
{/* 로딩 상태 표시 - 실시간 답변 생성 중에는 표시하지 않음 */}
|
||||
{loading && messages.length === 0 && (
|
||||
<div className="flex items-end gap-2 max-w-[75%] self-start mr-auto">
|
||||
<span className="shrink-0 flex items-center">
|
||||
<Bot className="w-8 h-8 text-gray-400" />
|
||||
<div className="mr-auto flex max-w-[75%] items-end gap-2 self-start">
|
||||
<span className="flex shrink-0 items-center">
|
||||
<Bot className="h-8 w-8 text-gray-400" />
|
||||
</span>
|
||||
<span className="p-3 rounded-md text-2xl bg-gray-100" style={{ maxWidth: "calc(100% - 2.5rem)" }}>
|
||||
<span
|
||||
className="rounded-md bg-gray-100 p-3 text-2xl"
|
||||
style={{ maxWidth: "calc(100% - 2.5rem)" }}
|
||||
>
|
||||
답변을 생성하고 있습니다...
|
||||
</span>
|
||||
</div>
|
||||
@@ -181,10 +201,10 @@ function ChatBotPage() {
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex items-center gap-2 p-4 border-t flex-shrink-0"
|
||||
className="flex flex-shrink-0 items-center gap-2 border-t p-4"
|
||||
>
|
||||
<Input
|
||||
className="flex-1 h-[3.2rem] !text-[1.5rem]"
|
||||
className="h-[3.2rem] flex-1 !text-[1.5rem]"
|
||||
placeholder="질문을 입력하세요..."
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
@@ -199,4 +219,4 @@ function ChatBotPage() {
|
||||
);
|
||||
}
|
||||
|
||||
export { ChatBotPage };
|
||||
export { ChatBotPage };
|
||||
|
||||
@@ -53,7 +53,9 @@ function DemandLoginModal({ open, onClose, onSuccess }: LoginModalProps) {
|
||||
|
||||
// ✅ 유효성/서버 에러 표시용
|
||||
const [errors, setErrors] = useState<Partial<Record<FieldKey, string>>>({});
|
||||
const [touched, setTouched] = useState<Partial<Record<FieldKey, boolean>>>({});
|
||||
const [touched, setTouched] = useState<Partial<Record<FieldKey, boolean>>>(
|
||||
{},
|
||||
);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -99,7 +101,8 @@ function DemandLoginModal({ open, onClose, onSuccess }: LoginModalProps) {
|
||||
|
||||
if (!b) next.bizNo = "사업자등록번호를 입력해주세요.";
|
||||
else if (!isBizNoValid(b))
|
||||
next.bizNo = "사업자등록번호 형식이 올바르지 않습니다. (예: 123-45-67890)";
|
||||
next.bizNo =
|
||||
"사업자등록번호 형식이 올바르지 않습니다. (예: 123-45-67890)";
|
||||
|
||||
if (!noWorker && !w) next.workerNo = "종사자등록번호를 입력해주세요.";
|
||||
|
||||
@@ -115,7 +118,7 @@ function DemandLoginModal({ open, onClose, onSuccess }: LoginModalProps) {
|
||||
const markAllTouched = () =>
|
||||
setTouched({ bizNo: true, workerNo: true, pw: true });
|
||||
|
||||
const submit = async (e: React.FormEvent) => {
|
||||
const submit = async (e: React.SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
markAllTouched();
|
||||
|
||||
@@ -138,7 +141,10 @@ function DemandLoginModal({ open, onClose, onSuccess }: LoginModalProps) {
|
||||
// --- 데모: 비밀번호가 "1234"면 성공 처리(원하면 제거) ---
|
||||
const serverOk = pw.trim() === "1234";
|
||||
if (!serverOk) {
|
||||
setErrors((p) => ({ ...p, pw: "잘못된 비밀번호입니다. (데모: 1234 입력 시 성공)" }));
|
||||
setErrors((p) => ({
|
||||
...p,
|
||||
pw: "잘못된 비밀번호입니다. (데모: 1234 입력 시 성공)",
|
||||
}));
|
||||
setTouched((p) => ({ ...p, pw: true }));
|
||||
return;
|
||||
}
|
||||
@@ -210,7 +216,9 @@ function DemandLoginModal({ open, onClose, onSuccess }: LoginModalProps) {
|
||||
validate();
|
||||
}}
|
||||
placeholder="사업자등록번호를 입력하세요. (예: 123-45-67890)"
|
||||
className={inputCls({ hasError: !!(touched.bizNo && errors.bizNo) })}
|
||||
className={inputCls({
|
||||
hasError: !!(touched.bizNo && errors.bizNo),
|
||||
})}
|
||||
/>
|
||||
{touched.bizNo && errors.bizNo && (
|
||||
<p className="mt-3 text-[14px] font-medium text-red-500">
|
||||
@@ -251,7 +259,8 @@ function DemandLoginModal({ open, onClose, onSuccess }: LoginModalProps) {
|
||||
placeholder="종사자등록번호를 입력하세요."
|
||||
className={inputCls({
|
||||
disabled: noWorker,
|
||||
hasError: !noWorker && !!(touched.workerNo && errors.workerNo),
|
||||
hasError:
|
||||
!noWorker && !!(touched.workerNo && errors.workerNo),
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -279,7 +288,9 @@ function DemandLoginModal({ open, onClose, onSuccess }: LoginModalProps) {
|
||||
validate();
|
||||
}}
|
||||
placeholder="비밀번호를 입력하세요."
|
||||
className={inputCls({ hasError: !!(touched.pw && errors.pw) })}
|
||||
className={inputCls({
|
||||
hasError: !!(touched.pw && errors.pw),
|
||||
})}
|
||||
/>
|
||||
{touched.pw && errors.pw && (
|
||||
<p className="mt-3 text-[14px] font-medium text-red-500">
|
||||
@@ -305,7 +316,9 @@ function DemandLoginModal({ open, onClose, onSuccess }: LoginModalProps) {
|
||||
{/* 비밀번호 초기화 요청 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => alert("TODO: 비밀번호 초기화 요청 화면/기능 연결")}
|
||||
onClick={() =>
|
||||
alert("TODO: 비밀번호 초기화 요청 화면/기능 연결")
|
||||
}
|
||||
className="mx-auto block text-[14px] text-[#767676] hover:text-gray-900"
|
||||
>
|
||||
비밀번호 초기화 요청
|
||||
@@ -439,7 +452,10 @@ export default function TopNav() {
|
||||
) : (
|
||||
<>
|
||||
<span className="text-base text-gray-700">
|
||||
<strong className="text-xl text-emerald-600">{userName}</strong>님 환영합니다.
|
||||
<strong className="text-xl text-emerald-600">
|
||||
{userName}
|
||||
</strong>
|
||||
님 환영합니다.
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
@@ -448,13 +464,13 @@ export default function TopNav() {
|
||||
>
|
||||
로그아웃
|
||||
<span aria-hidden>
|
||||
<Image
|
||||
src="/images/landing/icon_logout.png"
|
||||
alt="로그아웃 아이콘"
|
||||
width={24}
|
||||
height={24}
|
||||
className="h-6 w-6 object-contain"
|
||||
/>
|
||||
<Image
|
||||
src="/images/landing/icon_logout.png"
|
||||
alt="로그아웃 아이콘"
|
||||
width={24}
|
||||
height={24}
|
||||
className="h-6 w-6 object-contain"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useEffect, useId, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useDmStore } from "@/app/store";
|
||||
import type { DemandCompType } from "@common/business_components/ui/sdmInsertForm/form";
|
||||
import { sha512 } from 'js-sha512';
|
||||
import { sha512 } from "js-sha512";
|
||||
|
||||
type LoginModalProps = {
|
||||
open: boolean;
|
||||
@@ -21,13 +21,19 @@ function isBizNoValid(v: string) {
|
||||
}
|
||||
|
||||
function inputCls(opts: { hasError?: boolean; disabled?: boolean }) {
|
||||
const base = "h-[48px] w-full rounded-[4px] border px-5 text-[18px] outline-none";
|
||||
const base =
|
||||
"h-[48px] w-full rounded-[4px] border px-5 text-[18px] outline-none";
|
||||
if (opts.disabled) return `${base} border-gray-200 bg-gray-50 text-gray-400`;
|
||||
if (opts.hasError) return `${base} border-red-500 ring-1 ring-red-400 focus:border-red-500`;
|
||||
if (opts.hasError)
|
||||
return `${base} border-red-500 ring-1 ring-red-400 focus:border-red-500`;
|
||||
return `${base} border-gray-300 focus:border-sky-400`;
|
||||
}
|
||||
|
||||
export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps) {
|
||||
export default function LoginModal({
|
||||
open,
|
||||
onClose,
|
||||
onSuccess,
|
||||
}: LoginModalProps) {
|
||||
const titleId = useId();
|
||||
const router = useRouter();
|
||||
const { actionLogin } = useDmStore((state) => state);
|
||||
@@ -39,7 +45,9 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps
|
||||
|
||||
// ✅ 유효성/서버 에러 표시용
|
||||
const [errors, setErrors] = useState<Partial<Record<FieldKey, string>>>({});
|
||||
const [touched, setTouched] = useState<Partial<Record<FieldKey, boolean>>>({});
|
||||
const [touched, setTouched] = useState<Partial<Record<FieldKey, boolean>>>(
|
||||
{},
|
||||
);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -64,7 +72,8 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps
|
||||
}, [open, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (noWorker) { // 종사업자번호 해당없음
|
||||
if (noWorker) {
|
||||
// 종사업자번호 해당없음
|
||||
setWorkerNo(""); // 종사업자번호
|
||||
setErrors((p) => {
|
||||
const nextErrors = { ...p };
|
||||
@@ -122,7 +131,7 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps
|
||||
const markAllTouched = () =>
|
||||
setTouched({ bizNo: true, workerNo: true, pw: true });
|
||||
|
||||
const submit = async (e: React.FormEvent) => {
|
||||
const submit = async (e: React.SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
markAllTouched();
|
||||
|
||||
@@ -150,8 +159,10 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps
|
||||
};
|
||||
|
||||
const initPassword = () => {
|
||||
alert("관리자(031-0000-0000)에게 비밀번호 초기화 요청을 유선으로 하시기 바랍니다.");
|
||||
}
|
||||
alert(
|
||||
"관리자(031-0000-0000)에게 비밀번호 초기화 요청을 유선으로 하시기 바랍니다.",
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100]">
|
||||
@@ -212,7 +223,9 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps
|
||||
validate();
|
||||
}}
|
||||
placeholder="사업자등록번호를 입력하세요. (예: 123-45-67890)"
|
||||
className={inputCls({ hasError: !!(touched.bizNo && errors.bizNo) })}
|
||||
className={inputCls({
|
||||
hasError: !!(touched.bizNo && errors.bizNo),
|
||||
})}
|
||||
/>
|
||||
{touched.bizNo && errors.bizNo && (
|
||||
<p className="mt-3 text-[14px] font-medium text-red-500">
|
||||
@@ -253,7 +266,8 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps
|
||||
placeholder="종사자등록번호를 입력하세요."
|
||||
className={inputCls({
|
||||
disabled: noWorker,
|
||||
hasError: !noWorker && !!(touched.workerNo && errors.workerNo),
|
||||
hasError:
|
||||
!noWorker && !!(touched.workerNo && errors.workerNo),
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -281,7 +295,9 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps
|
||||
validate();
|
||||
}}
|
||||
placeholder="비밀번호를 입력하세요."
|
||||
className={inputCls({ hasError: !!(touched.pw && errors.pw) })}
|
||||
className={inputCls({
|
||||
hasError: !!(touched.pw && errors.pw),
|
||||
})}
|
||||
/>
|
||||
{touched.pw && errors.pw && (
|
||||
<p className="mt-3 text-[14px] font-medium text-red-500">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import useSWR from 'swr';
|
||||
import useSWR from "swr";
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -12,20 +12,29 @@ import {
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
Checkbox
|
||||
}
|
||||
from "@common/components";
|
||||
Checkbox,
|
||||
} from "@common/components";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { type SearchFormProps, type SearchFormValues, defaultSearchFormProps } from "../type/SearchForm.type";
|
||||
import {
|
||||
type SearchFormProps,
|
||||
type SearchFormValues,
|
||||
defaultSearchFormProps,
|
||||
} from "../type/SearchForm.type";
|
||||
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
|
||||
import type { ApiResponse, SearchCondition, Sgg } from "@/entities/statistics/model/statistics-interface";
|
||||
import type {
|
||||
ApiResponse,
|
||||
SearchCondition,
|
||||
Sgg,
|
||||
} from "@/entities/statistics/model/statistics-interface";
|
||||
|
||||
const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
|
||||
return statisticsAPi.getSidoList();
|
||||
};
|
||||
|
||||
const getSggListFetcher = async (key: [string, string]): Promise<ApiResponse<Sgg[]>> => {
|
||||
const getSggListFetcher = async (
|
||||
key: [string, string],
|
||||
): Promise<ApiResponse<Sgg[]>> => {
|
||||
return statisticsAPi.getSggList(key[1]);
|
||||
};
|
||||
|
||||
@@ -46,7 +55,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
dedupingInterval: 2000,
|
||||
shouldRetryOnError: false,
|
||||
onSuccess: (data) => {
|
||||
if (data.status !== 'success') {
|
||||
if (data.status !== "success") {
|
||||
alert(`시도 목록을 불러오는데 실패했습니다.
|
||||
새로고침 후 다시 시도해주세요.`);
|
||||
}
|
||||
@@ -55,23 +64,21 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
alert(`시도 목록을 불러오는데 실패했습니다.
|
||||
새로고침 후 다시 시도해주세요.`);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const [values, setValues] = useState<SearchFormValues>({
|
||||
sido: props.defaultValues?.sido || '',
|
||||
gugun: props.defaultValues?.gugun || '',
|
||||
sido: props.defaultValues?.sido || "",
|
||||
gugun: props.defaultValues?.gugun || "",
|
||||
year: props.defaultValues?.year || defaultYear.toString(),
|
||||
order: props.defaultValues?.order || '',
|
||||
fctName: props.defaultValues?.fctName || '',
|
||||
bsnmCd: props.defaultValues?.bsnmCd || '',
|
||||
order: props.defaultValues?.order || "",
|
||||
fctName: props.defaultValues?.fctName || "",
|
||||
bsnmCd: props.defaultValues?.bsnmCd || "",
|
||||
isDetail: false,
|
||||
});
|
||||
|
||||
const sggKey: [string, string] | null =
|
||||
values.sido && values.sido !== "ALL"
|
||||
? ["sgg-list", values.sido]
|
||||
: null;
|
||||
values.sido && values.sido !== "ALL" ? ["sgg-list", values.sido] : null;
|
||||
const { data: sggList } = useSWR<ApiResponse<Sgg[]>>(
|
||||
sggKey,
|
||||
getSggListFetcher,
|
||||
@@ -82,7 +89,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
dedupingInterval: 2000,
|
||||
shouldRetryOnError: false,
|
||||
onSuccess: (data) => {
|
||||
if (data.status !== 'success') {
|
||||
if (data.status !== "success") {
|
||||
alert(`시군구 목록을 불러오는데 실패했습니다.
|
||||
새로고침 후 다시 시도해주세요.`);
|
||||
}
|
||||
@@ -91,23 +98,23 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
alert(`시군구 목록을 불러오는데 실패했습니다.
|
||||
새로고침 후 다시 시도해주세요.`);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
const sggOptions = sggList?.data ?? [];
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
props.onSubmit?.(values);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
const resetValues: SearchFormValues = {
|
||||
sido: '',
|
||||
gugun: '',
|
||||
sido: "",
|
||||
gugun: "",
|
||||
year: String(defaultYear),
|
||||
order: '',
|
||||
fctName: '',
|
||||
bsnmCd: '',
|
||||
order: "",
|
||||
fctName: "",
|
||||
bsnmCd: "",
|
||||
isDetail: false,
|
||||
};
|
||||
setValues(resetValues);
|
||||
@@ -115,20 +122,23 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
};
|
||||
|
||||
const handleSidoChange = (value: string) => {
|
||||
const selectedBsnm = sidoList?.data.sidoList.find((v) => v.sido === value)?.bsnmCd || '';
|
||||
const selectedBsnm =
|
||||
sidoList?.data.sidoList.find((v) => v.sido === value)?.bsnmCd || "";
|
||||
setValues((prev) => ({
|
||||
...prev,
|
||||
sido: value,
|
||||
gugun: '',
|
||||
gugun: "",
|
||||
bsnmCd: selectedBsnm,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSggChange = (value: string) => {
|
||||
const listForBsnm = sggList?.data ?? [];
|
||||
const selectedBsnm = value === 'ALL'
|
||||
? (sidoList?.data.sidoList.find((v) => v.sido === values.sido)?.bsnmCd || '')
|
||||
: (listForBsnm.find((v) => v.gugun === value)?.bsnmCd || '');
|
||||
const selectedBsnm =
|
||||
value === "ALL"
|
||||
? sidoList?.data.sidoList.find((v) => v.sido === values.sido)
|
||||
?.bsnmCd || ""
|
||||
: listForBsnm.find((v) => v.gugun === value)?.bsnmCd || "";
|
||||
|
||||
setValues((prev) => ({
|
||||
...prev,
|
||||
@@ -145,66 +155,80 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="flex items-center gap-8 p-2">
|
||||
{mergedProps.sido &&
|
||||
{mergedProps.sido && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label">
|
||||
시도
|
||||
</Label>
|
||||
<Select value={values.sido} onValueChange={handleSidoChange}>
|
||||
<Label className="text-label">시도</Label>
|
||||
<Select
|
||||
value={values.sido}
|
||||
onValueChange={handleSidoChange}
|
||||
>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="선택"></SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">선택</SelectItem>
|
||||
{sidoList?.data?.sidoList?.map((sido) => (
|
||||
<SelectItem key={sido.adstDstrCd} value={sido.sido}>
|
||||
<SelectItem
|
||||
key={sido.adstDstrCd}
|
||||
value={sido.sido}
|
||||
>
|
||||
{sido.sido}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
}
|
||||
{mergedProps.gugun &&
|
||||
)}
|
||||
{mergedProps.gugun && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label">
|
||||
시군구
|
||||
</Label>
|
||||
<Select value={values.gugun} onValueChange={handleSggChange}>
|
||||
<Label className="text-label">시군구</Label>
|
||||
<Select
|
||||
value={values.gugun}
|
||||
onValueChange={handleSggChange}
|
||||
>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="전체"></SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
{sggOptions?.map((sgg) => (
|
||||
<SelectItem key={sgg.adstDstrCd} value={sgg.gugun}>
|
||||
<SelectItem
|
||||
key={sgg.adstDstrCd}
|
||||
value={sgg.gugun}
|
||||
>
|
||||
{sgg.gugun}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
}
|
||||
{mergedProps.year &&
|
||||
)}
|
||||
{mergedProps.year && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label">
|
||||
년도
|
||||
</Label>
|
||||
<Select value={values.year} onValueChange={(value) => setValues({ ...values, year: value })}>
|
||||
<Label className="text-label">년도</Label>
|
||||
<Select
|
||||
value={values.year}
|
||||
onValueChange={(value) =>
|
||||
setValues({ ...values, year: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[10rem]">
|
||||
<SelectValue placeholder="선택"></SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sidoList?.data?.yearList.map((year) => (
|
||||
<SelectItem key={year} value={String(year)}>
|
||||
<SelectItem
|
||||
key={year}
|
||||
value={String(year)}
|
||||
>
|
||||
{year}년
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
}
|
||||
{mergedProps.isDetail &&
|
||||
)}
|
||||
{mergedProps.isDetail && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label">
|
||||
세부 데이터 포함
|
||||
@@ -212,17 +236,23 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
<Checkbox
|
||||
checked={values.isDetail}
|
||||
onCheckedChange={(value) =>
|
||||
setValues({ ...values, isDetail: value === true })
|
||||
setValues({
|
||||
...values,
|
||||
isDetail: value === true,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{mergedProps.order &&
|
||||
)}
|
||||
{mergedProps.order && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label">
|
||||
차수
|
||||
</Label>
|
||||
<Select value={values.order} onValueChange={(value) => setValues({ ...values, order: value })}>
|
||||
<Label className="text-label">차수</Label>
|
||||
<Select
|
||||
value={values.order}
|
||||
onValueChange={(value) =>
|
||||
setValues({ ...values, order: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[10rem]">
|
||||
<SelectValue placeholder="전체"></SelectValue>
|
||||
</SelectTrigger>
|
||||
@@ -231,9 +261,14 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
}
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<Button color="white" className="h-[3.2rem] w-[96px]" type="button" onClick={handleReset}>
|
||||
)}
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Button
|
||||
color="white"
|
||||
className="h-[3.2rem] w-[96px]"
|
||||
type="button"
|
||||
onClick={handleReset}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
<Button className="h-[3.2rem] w-[96px]" type="submit">
|
||||
@@ -241,35 +276,46 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{(mergedProps.fctName || mergedProps.userMenual) &&
|
||||
{(mergedProps.fctName || mergedProps.userMenual) && (
|
||||
<div className="flex items-center p-2">
|
||||
{mergedProps.fctName &&
|
||||
{mergedProps.fctName && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="fctNmSearchInput" className="mr-2 text-label">
|
||||
<Label
|
||||
htmlFor="fctNmSearchInput"
|
||||
className="mr-2 text-label"
|
||||
>
|
||||
시설명
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[14rem] rounded-sm !text-[1.3rem] mr-4"
|
||||
className="mr-4 h-[3.2rem] w-[14rem] rounded-sm !text-[1.3rem]"
|
||||
id="fctNmSearchInput"
|
||||
value={values.fctName}
|
||||
onChange={(e) => setValues({ ...values, fctName: e.target.value })}
|
||||
onChange={(e) =>
|
||||
setValues({
|
||||
...values,
|
||||
fctName: e.target.value,
|
||||
})
|
||||
}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{mergedProps.userMenual &&
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<Button className="h-[3.2rem] w-[124px]" type="button">
|
||||
)}
|
||||
{mergedProps.userMenual && (
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Button
|
||||
className="h-[3.2rem] w-[124px]"
|
||||
type="button"
|
||||
>
|
||||
<i className="diveicon di-person-fill" />
|
||||
사용자 메뉴얼
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchForm;
|
||||
export default SearchForm;
|
||||
|
||||
@@ -13,14 +13,18 @@ import {
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
LoadingWithAnimation
|
||||
LoadingWithAnimation,
|
||||
} from "@common/components";
|
||||
import CompactDataGrid from "./CompactDataGrid";
|
||||
import SaveButton from "../button/SaveButton";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
|
||||
import type { ApiResponse, SearchCondition, Sgg } from "@/entities/statistics/model/statistics-interface";
|
||||
import type {
|
||||
ApiResponse,
|
||||
SearchCondition,
|
||||
Sgg,
|
||||
} from "@/entities/statistics/model/statistics-interface";
|
||||
import type { ColumnInfo } from "../type/DataGrid.type";
|
||||
import type {
|
||||
SeweragePipeFacilitiesListRequest,
|
||||
@@ -31,9 +35,24 @@ import { statisticsInputSewPipeApiService } from "@/entities/statistics/api/inpu
|
||||
/** 다이얼로그 좌측: 처리시설 정보 (순번 없음) */
|
||||
const treatmentFacilityColumnInfo: ColumnInfo = {
|
||||
columns: [
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
|
||||
{ name: "fctNm", fieldName: "fctNm", header: { text: "시설명" }, editable: false },
|
||||
{
|
||||
name: "ctpvNm",
|
||||
fieldName: "ctpvNm",
|
||||
header: { text: "시도" },
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "sggNm",
|
||||
fieldName: "sggNm",
|
||||
header: { text: "시군구" },
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "fctNm",
|
||||
fieldName: "fctNm",
|
||||
header: { text: "시설명" },
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "fctCpc",
|
||||
fieldName: "fctCpc",
|
||||
@@ -49,9 +68,24 @@ const treatmentFacilityColumnInfo: ColumnInfo = {
|
||||
/** 다이얼로그 우측: 행정구역 정보 (순번 없음, 헤더 체크박스는 showHeaderCheckbox) */
|
||||
const adminRegionColumnInfo: ColumnInfo = {
|
||||
columns: [
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
|
||||
{ name: "emdNm", fieldName: "emdNm", header: { text: "읍면동" }, editable: false },
|
||||
{
|
||||
name: "ctpvNm",
|
||||
fieldName: "ctpvNm",
|
||||
header: { text: "시도" },
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "sggNm",
|
||||
fieldName: "sggNm",
|
||||
header: { text: "시군구" },
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "emdNm",
|
||||
fieldName: "emdNm",
|
||||
header: { text: "읍면동" },
|
||||
editable: false,
|
||||
},
|
||||
],
|
||||
columnLayout: ["ctpvNm", "sggNm", "emdNm"],
|
||||
};
|
||||
@@ -60,7 +94,9 @@ const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
|
||||
return statisticsAPi.getSidoList();
|
||||
};
|
||||
|
||||
const getSggAllListFetcher = async (key: [string, string]): Promise<ApiResponse<Sgg[]>> => {
|
||||
const getSggAllListFetcher = async (
|
||||
key: [string, string],
|
||||
): Promise<ApiResponse<Sgg[]>> => {
|
||||
return statisticsAPi.getSggAllList(key[1]);
|
||||
};
|
||||
|
||||
@@ -69,49 +105,72 @@ type SidoSggFacilityNameSearchFormProps = {
|
||||
statsYr?: string;
|
||||
};
|
||||
|
||||
const SidoSggFacilityNameSearchForm = ({ statsYr }: SidoSggFacilityNameSearchFormProps) => {
|
||||
const SidoSggFacilityNameSearchForm = ({
|
||||
statsYr,
|
||||
}: SidoSggFacilityNameSearchFormProps) => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { data: sidoList } = useSWR<ApiResponse<SearchCondition>>("sido-list", getSidoListFetcher, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
dedupingInterval: 2000,
|
||||
shouldRetryOnError: false,
|
||||
onSuccess: (data) => {
|
||||
if (data.status !== "success") {
|
||||
alert(`시도 목록을 불러오는데 실패했습니다.\n새로고침 후 다시 시도해주세요.`);
|
||||
}
|
||||
const { data: sidoList } = useSWR<ApiResponse<SearchCondition>>(
|
||||
"sido-list",
|
||||
getSidoListFetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
dedupingInterval: 2000,
|
||||
shouldRetryOnError: false,
|
||||
onSuccess: (data) => {
|
||||
if (data.status !== "success") {
|
||||
alert(
|
||||
`시도 목록을 불러오는데 실패했습니다.\n새로고침 후 다시 시도해주세요.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
alert(
|
||||
`시도 목록을 불러오는데 실패했습니다.\n새로고침 후 다시 시도해주세요.`,
|
||||
);
|
||||
},
|
||||
},
|
||||
onError: () => {
|
||||
alert(`시도 목록을 불러오는데 실패했습니다.\n새로고침 후 다시 시도해주세요.`);
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const [sido, setSido] = useState<string>("ALL");
|
||||
const [gugun, setGugun] = useState<string>("ALL");
|
||||
const [fctNm, setFctNm] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const [treatmentFacilityRows, setTreatmentFacilityRows] = useState<Record<string, string | number>[]>([]);
|
||||
const [adminRegionRows, setAdminRegionRows] = useState<Record<string, string | number>[]>([]);
|
||||
const [treatmentFacilityRows, setTreatmentFacilityRows] = useState<
|
||||
Record<string, string | number>[]
|
||||
>([]);
|
||||
const [adminRegionRows, setAdminRegionRows] = useState<
|
||||
Record<string, string | number>[]
|
||||
>([]);
|
||||
|
||||
const sggKey: [string, string] | null = sido && sido !== "ALL" ? ["sgg-all", sido] : null;
|
||||
const { data: sggList } = useSWR<ApiResponse<Sgg[]>>(sggKey, getSggAllListFetcher, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
dedupingInterval: 2000,
|
||||
shouldRetryOnError: false,
|
||||
onSuccess: (data) => {
|
||||
if (data.status !== "success") {
|
||||
alert(`시군구 목록을 불러오는데 실패했습니다.\n새로고침 후 다시 시도해주세요.`);
|
||||
}
|
||||
const sggKey: [string, string] | null =
|
||||
sido && sido !== "ALL" ? ["sgg-all", sido] : null;
|
||||
const { data: sggList } = useSWR<ApiResponse<Sgg[]>>(
|
||||
sggKey,
|
||||
getSggAllListFetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
dedupingInterval: 2000,
|
||||
shouldRetryOnError: false,
|
||||
onSuccess: (data) => {
|
||||
if (data.status !== "success") {
|
||||
alert(
|
||||
`시군구 목록을 불러오는데 실패했습니다.\n새로고침 후 다시 시도해주세요.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
alert(
|
||||
`시군구 목록을 불러오는데 실패했습니다.\n새로고침 후 다시 시도해주세요.`,
|
||||
);
|
||||
},
|
||||
},
|
||||
onError: () => {
|
||||
alert(`시군구 목록을 불러오는데 실패했습니다.\n새로고침 후 다시 시도해주세요.`);
|
||||
},
|
||||
});
|
||||
);
|
||||
const sggOptions = sggList?.data ?? [];
|
||||
|
||||
/** 시설 목록·행정구역 API 공통 — 하수도 사업자코드(bsnmCd) */
|
||||
@@ -129,7 +188,7 @@ const SidoSggFacilityNameSearchForm = ({ statsYr }: SidoSggFacilityNameSearchFor
|
||||
setGugun("ALL");
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = async (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!statsYr?.trim()) {
|
||||
alert("상단에서 통계연도로 조회한 뒤 이용해 주세요.");
|
||||
@@ -153,7 +212,10 @@ const SidoSggFacilityNameSearchForm = ({ statsYr }: SidoSggFacilityNameSearchFor
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await statisticsInputSewPipeApiService.getFacilitiesList(params);
|
||||
const response =
|
||||
await statisticsInputSewPipeApiService.getFacilitiesList(
|
||||
params,
|
||||
);
|
||||
if (response.status !== "success") {
|
||||
alert(response.message ?? "처리시설 목록 조회에 실패했습니다.");
|
||||
return;
|
||||
@@ -178,7 +240,9 @@ const SidoSggFacilityNameSearchForm = ({ statsYr }: SidoSggFacilityNameSearchFor
|
||||
}
|
||||
};
|
||||
|
||||
const handleTreatmentFacilityRowClick = async (row: Record<string, string | number>) => {
|
||||
const handleTreatmentFacilityRowClick = async (
|
||||
row: Record<string, string | number>,
|
||||
) => {
|
||||
if (!statsYr?.trim()) {
|
||||
alert("상단에서 통계연도로 조회한 뒤 이용해 주세요.");
|
||||
return;
|
||||
@@ -197,14 +261,17 @@ const SidoSggFacilityNameSearchForm = ({ statsYr }: SidoSggFacilityNameSearchFor
|
||||
const params: SeweragePipeFacilityAdminAreaListRequest = {
|
||||
statsYr: statsYr.trim(),
|
||||
swtrmtFcltCd,
|
||||
sewerBzmnCd: String(row.admdstCd ?? "").trim(),
|
||||
sewerBzmnCd: String(row.admdstCd ?? "").trim(),
|
||||
ctpvNm,
|
||||
sggNm,
|
||||
};
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await statisticsInputSewPipeApiService.getFacilityAdminAreaList(params);
|
||||
const response =
|
||||
await statisticsInputSewPipeApiService.getFacilityAdminAreaList(
|
||||
params,
|
||||
);
|
||||
if (response.status !== "success") {
|
||||
alert(response.message ?? "행정구역 목록 조회에 실패했습니다.");
|
||||
return;
|
||||
@@ -234,7 +301,7 @@ const SidoSggFacilityNameSearchForm = ({ statsYr }: SidoSggFacilityNameSearchFor
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label shrink-0">시도</Label>
|
||||
<Label className="shrink-0 text-label">시도</Label>
|
||||
<Select value={sido} onValueChange={handleSidoChange}>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="전체" />
|
||||
@@ -242,7 +309,10 @@ const SidoSggFacilityNameSearchForm = ({ statsYr }: SidoSggFacilityNameSearchFor
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
{sidoList?.data?.sidoList?.map((item) => (
|
||||
<SelectItem key={item.adstDstrCd} value={item.sido}>
|
||||
<SelectItem
|
||||
key={item.adstDstrCd}
|
||||
value={item.sido}
|
||||
>
|
||||
{item.sido}
|
||||
</SelectItem>
|
||||
))}
|
||||
@@ -250,31 +320,42 @@ const SidoSggFacilityNameSearchForm = ({ statsYr }: SidoSggFacilityNameSearchFor
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label shrink-0">시군구</Label>
|
||||
<Select value={gugun} onValueChange={setGugun} disabled={!sido || sido === "ALL"}>
|
||||
<Label className="shrink-0 text-label">시군구</Label>
|
||||
<Select
|
||||
value={gugun}
|
||||
onValueChange={setGugun}
|
||||
disabled={!sido || sido === "ALL"}
|
||||
>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="전체" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
{sggOptions?.map((sgg) => (
|
||||
<SelectItem key={sgg.adstDstrCd} value={sgg.gugun}>
|
||||
<SelectItem
|
||||
key={sgg.adstDstrCd}
|
||||
value={sgg.gugun}
|
||||
>
|
||||
{sgg.gugun}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 min-w-[20rem] flex-1">
|
||||
<Label className="text-label shrink-0">시설명</Label>
|
||||
<div className="flex min-w-[20rem] flex-1 items-center gap-2">
|
||||
<Label className="shrink-0 text-label">시설명</Label>
|
||||
<Input
|
||||
className="flex-1 min-w-[12rem]"
|
||||
className="min-w-[12rem] flex-1"
|
||||
value={fctNm}
|
||||
onChange={(e) => setFctNm(e.target.value)}
|
||||
placeholder="시설명 입력"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" className="h-[3.2rem] w-[96px]" color="primary">
|
||||
<Button
|
||||
type="submit"
|
||||
className="h-[3.2rem] w-[96px]"
|
||||
color="primary"
|
||||
>
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
@@ -282,14 +363,18 @@ const SidoSggFacilityNameSearchForm = ({ statsYr }: SidoSggFacilityNameSearchFor
|
||||
<SaveButton saveFunc={handleSaveAdminRegions} />
|
||||
</div>
|
||||
</form>
|
||||
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
{isLoading && (
|
||||
<div className="[data-theme='black']:bg-transparent absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
<div className="flex gap-[1.2rem] items-stretch w-full min-w-0">
|
||||
<div className="flex-1 flex flex-col gap-2 min-w-0">
|
||||
<h3 className="text-[1.4rem] font-medium text-foreground">처리시설 정보</h3>
|
||||
)}
|
||||
<div className="flex w-full min-w-0 items-stretch gap-[1.2rem]">
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||||
<h3 className="text-[1.4rem] font-medium text-foreground">
|
||||
처리시설 정보
|
||||
</h3>
|
||||
<CompactDataGrid
|
||||
columnInfo={treatmentFacilityColumnInfo}
|
||||
rowData={treatmentFacilityRows}
|
||||
@@ -300,9 +385,11 @@ const SidoSggFacilityNameSearchForm = ({ statsYr }: SidoSggFacilityNameSearchFor
|
||||
// compactGridHeightClass="h-[min(40vh,28rem)] min-h-[16rem]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-2 min-w-0">
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h3 className="text-[1.4rem] font-medium text-foreground">행정구역 정보</h3>
|
||||
<h3 className="text-[1.4rem] font-medium text-foreground">
|
||||
행정구역 정보
|
||||
</h3>
|
||||
</div>
|
||||
<CompactDataGrid
|
||||
columnInfo={adminRegionColumnInfo}
|
||||
|
||||
@@ -1,60 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Label,
|
||||
LoadingWithAnimation,
|
||||
}
|
||||
from "@common/components";
|
||||
import DataGrid from '@/components/module/DataGrid';
|
||||
import { CodeSelectWithAll } from '@/entities/code';
|
||||
import { Button, Label, LoadingWithAnimation } from "@common/components";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import { CodeSelectWithAll } from "@/entities/code";
|
||||
|
||||
import { statisticsAPi } from '@/entities/statistics/api/statistics-service';
|
||||
import type { StatisticsLogRequest } from '@/entities/statistics/model/statistics-interface';
|
||||
import { toGridRows } from '@/components/util/grid/gridFunction';
|
||||
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
|
||||
import type { StatisticsLogRequest } from "@/entities/statistics/model/statistics-interface";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
import { LogHistoryColumn } from '@/components/util/column/management/LogHistoryColumn';
|
||||
import { LogHistoryTablePagination } from '@/components/statistics/management/LogHistoryTablePagination';
|
||||
import { LogHistoryColumn } from "@/components/util/column/management/LogHistoryColumn";
|
||||
import { LogHistoryTablePagination } from "@/components/statistics/management/LogHistoryTablePagination";
|
||||
|
||||
const PAGE_SIZE = 100;
|
||||
|
||||
const LogHistory = () => {
|
||||
// const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [statsCd, setStatsCd] = useState<string>('');
|
||||
const [statsCd, setStatsCd] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>(
|
||||
[],
|
||||
);
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
|
||||
const loadLogs = useCallback(async (page1Based: number) => {
|
||||
const param: StatisticsLogRequest = {
|
||||
statsCd: (statsCd === '' || statsCd === 'ALL') ? '' : statsCd,
|
||||
page: page1Based,
|
||||
size: PAGE_SIZE,
|
||||
};
|
||||
const loadLogs = useCallback(
|
||||
async (page1Based: number) => {
|
||||
const param: StatisticsLogRequest = {
|
||||
statsCd: statsCd === "" || statsCd === "ALL" ? "" : statsCd,
|
||||
page: page1Based,
|
||||
size: PAGE_SIZE,
|
||||
};
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const response = await statisticsAPi.getLogList(param);
|
||||
const response = await statisticsAPi.getLogList(param);
|
||||
|
||||
if (response.status === 'success') {
|
||||
setRowData(toGridRows(response.data?.statsLogList ?? []));
|
||||
setTotalPages(response.data?.pageInfo?.totalPages ?? 0);
|
||||
setPageIndex(Math.max(0, page1Based - 1));
|
||||
} else {
|
||||
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
|
||||
if (response.status === "success") {
|
||||
setRowData(toGridRows(response.data?.statsLogList ?? []));
|
||||
setTotalPages(response.data?.pageInfo?.totalPages ?? 0);
|
||||
setPageIndex(Math.max(0, page1Based - 1));
|
||||
} else {
|
||||
alert(
|
||||
response.message ||
|
||||
"데이터 조회 중 오류가 발생했습니다.",
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [statsCd]);
|
||||
},
|
||||
[statsCd],
|
||||
);
|
||||
|
||||
const handleSearch = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSearch = async (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
await loadLogs(1);
|
||||
};
|
||||
@@ -65,9 +68,7 @@ const LogHistory = () => {
|
||||
<form onSubmit={handleSearch}>
|
||||
<div className="flex items-center gap-8 p-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label">
|
||||
통계항목
|
||||
</Label>
|
||||
<Label className="text-label">통계항목</Label>
|
||||
<CodeSelectWithAll
|
||||
upCd="STITEM"
|
||||
value={statsCd}
|
||||
@@ -76,27 +77,36 @@ const LogHistory = () => {
|
||||
className="w-[16rem]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<Button color="green" className="h-[3.2rem] w-[96px]" type="button">
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Button
|
||||
color="green"
|
||||
className="h-[3.2rem] w-[96px]"
|
||||
type="button"
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
<Button className="h-[3.2rem] w-[96px]" type="submit">
|
||||
<Button
|
||||
className="h-[3.2rem] w-[96px]"
|
||||
type="submit"
|
||||
>
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
{isLoading && (
|
||||
<div className="[data-theme='black']:bg-transparent absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
)}
|
||||
<DataGrid
|
||||
columnInfo={LogHistoryColumn}
|
||||
isExcelImport={false}
|
||||
rowData={rowData}
|
||||
setRowData={() => { }}
|
||||
setRowData={() => {}}
|
||||
/>
|
||||
<LogHistoryTablePagination
|
||||
pageCount={totalPages}
|
||||
@@ -109,4 +119,4 @@ const LogHistory = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default LogHistory;
|
||||
export default LogHistory;
|
||||
|
||||
@@ -211,7 +211,7 @@ function VerificationArticleManagement() {
|
||||
setDialogOpen(true);
|
||||
}
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
function handleSubmit(e: React.SubmitEvent) {
|
||||
e.preventDefault();
|
||||
handleSearch();
|
||||
}
|
||||
|
||||
@@ -6,124 +6,132 @@ import { LoginApi } from "@/entities/auth";
|
||||
import "./login.css";
|
||||
|
||||
function LoginPage() {
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const handleSubmit = async (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const userId = formData.get("id") as string;
|
||||
const password = formData.get("password") as string;
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const userId = formData.get("id") as string;
|
||||
const password = formData.get("password") as string;
|
||||
|
||||
await LoginApi.login({ userId, password });
|
||||
window.location.href = "/statistics";
|
||||
};
|
||||
await LoginApi.login({ userId, password });
|
||||
window.location.href = "/statistics";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login fx">
|
||||
<div className="left_box fx">
|
||||
<div className="text">
|
||||
<h1>
|
||||
유역하수도통합플랫폼
|
||||
<br />
|
||||
<span>하수도통계 관리시스템</span>
|
||||
</h1>
|
||||
<p>유역하수도통합플랫폼 하수도통계 관리시스템에 오신 것을 환영합니다.</p>
|
||||
</div>
|
||||
<div className="img">
|
||||
<Image src={images.logo} width={229} height={46} alt="logo" />
|
||||
</div>
|
||||
</div>
|
||||
<form
|
||||
id="loginForm"
|
||||
name="loginForm"
|
||||
method="post"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className="login_box fx">
|
||||
<ul className="fx">
|
||||
<li>
|
||||
<h2>ID</h2>
|
||||
<div className="input_st st1">
|
||||
<i className="diveicon di-account-box" />
|
||||
<input
|
||||
type="text"
|
||||
className="id"
|
||||
title="아이디를 입력하세요."
|
||||
id="id"
|
||||
name="id"
|
||||
maxLength={12}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<h2>PASSWORD</h2>
|
||||
<div className="input_st st2">
|
||||
<i className="diveicon di-key" />
|
||||
<input
|
||||
type="password"
|
||||
className="input_style"
|
||||
maxLength={25}
|
||||
title="비밀번호를 입력하세요."
|
||||
id="password"
|
||||
name="password"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="bottom_box fx">
|
||||
<div className="checks fx">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="checkId"
|
||||
title="로그인ID 저장여부"
|
||||
id="checkId"
|
||||
/>
|
||||
<label htmlFor="checkId">아이디저장</label>
|
||||
return (
|
||||
<div className="login fx">
|
||||
<div className="left_box fx">
|
||||
<div className="text">
|
||||
<h1>
|
||||
유역하수도통합플랫폼
|
||||
<br />
|
||||
<span>하수도통계 관리시스템</span>
|
||||
</h1>
|
||||
<p>
|
||||
유역하수도통합플랫폼 하수도통계 관리시스템에 오신 것을
|
||||
환영합니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className="img">
|
||||
<Image
|
||||
src={images.logo}
|
||||
width={229}
|
||||
height={46}
|
||||
alt="logo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* <a href="javascript:fnUserSearch();" className=" pw_message fx"><i className="xi-external-link"></i>비밀번호 또는 아이디를 잃어버리셨나요?</a> */}
|
||||
</div>
|
||||
<button type="submit" className="login_btn fx">
|
||||
<span>LOGIN</span>
|
||||
</button>
|
||||
<div className="footer">
|
||||
<div className="copy fx">
|
||||
<p>copyright © 2023 KECO. All rights reserved.</p>
|
||||
</div>
|
||||
<div className="fot_btn fx">
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1348/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
개인정보처리방침
|
||||
</a>
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1349/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
저작권정책안내
|
||||
</a>
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1350/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
이메일무단수집거부
|
||||
</a>
|
||||
</div>
|
||||
<p className="addr fx">
|
||||
<span>
|
||||
주소 : (22689) 인천광역시 서구 환경로 42(경서동
|
||||
종합환경연구단지)
|
||||
</span>
|
||||
<span>
|
||||
전화 : (시스템) 032-590-4321, 3752, 4314 / (통 계) 032-590-3723,
|
||||
3722
|
||||
</span>
|
||||
<span>팩스 :032-590-3729</span>
|
||||
</p>
|
||||
</div>
|
||||
<form
|
||||
id="loginForm"
|
||||
name="loginForm"
|
||||
method="post"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className="login_box fx">
|
||||
<ul className="fx">
|
||||
<li>
|
||||
<h2>ID</h2>
|
||||
<div className="input_st st1">
|
||||
<i className="diveicon di-account-box" />
|
||||
<input
|
||||
type="text"
|
||||
className="id"
|
||||
title="아이디를 입력하세요."
|
||||
id="id"
|
||||
name="id"
|
||||
maxLength={12}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<h2>PASSWORD</h2>
|
||||
<div className="input_st st2">
|
||||
<i className="diveicon di-key" />
|
||||
<input
|
||||
type="password"
|
||||
className="input_style"
|
||||
maxLength={25}
|
||||
title="비밀번호를 입력하세요."
|
||||
id="password"
|
||||
name="password"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="bottom_box fx">
|
||||
<div className="checks fx">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="checkId"
|
||||
title="로그인ID 저장여부"
|
||||
id="checkId"
|
||||
/>
|
||||
<label htmlFor="checkId">아이디저장</label>
|
||||
</div>
|
||||
{/* <a href="javascript:fnUserSearch();" className=" pw_message fx"><i className="xi-external-link"></i>비밀번호 또는 아이디를 잃어버리셨나요?</a> */}
|
||||
</div>
|
||||
<button type="submit" className="login_btn fx">
|
||||
<span>LOGIN</span>
|
||||
</button>
|
||||
<div className="footer">
|
||||
<div className="copy fx">
|
||||
<p>copyright © 2023 KECO. All rights reserved.</p>
|
||||
</div>
|
||||
<div className="fot_btn fx">
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1348/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
개인정보처리방침
|
||||
</a>
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1349/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
저작권정책안내
|
||||
</a>
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1350/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
이메일무단수집거부
|
||||
</a>
|
||||
</div>
|
||||
<p className="addr fx">
|
||||
<span>
|
||||
주소 : (22689) 인천광역시 서구 환경로 42(경서동
|
||||
종합환경연구단지)
|
||||
</span>
|
||||
<span>
|
||||
전화 : (시스템) 032-590-4321, 3752, 4314 / (통
|
||||
계) 032-590-3723, 3722
|
||||
</span>
|
||||
<span>팩스 :032-590-3729</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
export { LoginPage };
|
||||
|
||||
@@ -2,18 +2,13 @@ import { numSize } from "@/app/store/common/code/basicCode";
|
||||
import type { TableUpperProps } from "@/app/store/common/type/header";
|
||||
import type { rainChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn";
|
||||
import type { rswtChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rswrtChkgdn";
|
||||
import {
|
||||
Checkbox,
|
||||
Input,
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from "@common/components/ui";
|
||||
import { Checkbox, Input, TableCell, TableRow } from "@common/components/ui";
|
||||
import moment from "moment";
|
||||
import {
|
||||
codeToText,
|
||||
formatCommaNumber,
|
||||
formatYyyymmdd,
|
||||
handleCommaNumberChange
|
||||
handleCommaNumberChange,
|
||||
} from "./common";
|
||||
|
||||
export const rainChkTableData = (chkData: rainChkZodType[]) => {
|
||||
@@ -73,10 +68,10 @@ export const rswtChkTableData = (chkData: rswtChkZodType[]) => {
|
||||
root.rswtrDetails.forEach((detail) => {
|
||||
let flatRswtChk = {};
|
||||
const findGuide = root.guideDetails.find(
|
||||
(guide) => guide.utztnUsgCd === detail.utztnUsgCd
|
||||
)
|
||||
(guide) => guide.utztnUsgCd === detail.utztnUsgCd,
|
||||
);
|
||||
const findPoluttion = root.rswtrPollutions.find(
|
||||
(polution) => polution.utztnUsgCd === detail.utztnUsgCd
|
||||
(polution) => polution.utztnUsgCd === detail.utztnUsgCd,
|
||||
);
|
||||
flatRswtChk = {
|
||||
...root,
|
||||
@@ -84,7 +79,7 @@ export const rswtChkTableData = (chkData: rswtChkZodType[]) => {
|
||||
...findGuide,
|
||||
...findPoluttion,
|
||||
...flatRswtChk,
|
||||
utztnUsgCdText: codeToText(detail.utztnUsgCd, 'dmdUszCd'),
|
||||
utztnUsgCdText: codeToText(detail.utztnUsgCd, "dmdUszCd"),
|
||||
};
|
||||
makeRswtData.push(flatRswtChk);
|
||||
});
|
||||
@@ -101,12 +96,12 @@ export const chRowspanTable = (
|
||||
handleChange: (
|
||||
rowIndex: number,
|
||||
field: keyof (typeof makeTableData)[0],
|
||||
value: string | number
|
||||
value: string | number,
|
||||
) => void,
|
||||
toggleSelection: (pk: number, checked: boolean) => void,
|
||||
selectedIds: number[],
|
||||
handleCellClick: (index: number, headId: string) => void,
|
||||
disable?: boolean
|
||||
disable?: boolean,
|
||||
) => {
|
||||
let order = 0;
|
||||
// rowspan된 셀 렌더링 여부 추적용
|
||||
@@ -140,7 +135,11 @@ export const chRowspanTable = (
|
||||
key={key}
|
||||
rowSpan={rowspan}
|
||||
className="align-middle text-center whitespace-normal break-words"
|
||||
style={{ width: head.widthPercent ? `${head.widthPercent}%` : undefined }}
|
||||
style={{
|
||||
width: head.widthPercent
|
||||
? `${head.widthPercent}%`
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{key === "checkBox" ? (
|
||||
<Checkbox
|
||||
@@ -153,16 +152,16 @@ export const chRowspanTable = (
|
||||
) : key === "order" ? (
|
||||
++order
|
||||
) : // value
|
||||
["anlOperCst", "dtfcltCpct", "prcsCpct"].includes( //연간운영비, 저류조용량, 처리용량
|
||||
head.id
|
||||
["anlOperCst", "dtfcltCpct", "prcsCpct"].includes(
|
||||
//연간운영비, 저류조용량, 처리용량
|
||||
head.id,
|
||||
) ? (
|
||||
formatCommaNumber(value)
|
||||
) : key === "chckYmd" ? (
|
||||
<>{formatYyyymmdd(value)}</>
|
||||
) :
|
||||
(
|
||||
<>{value}</>
|
||||
)}
|
||||
formatCommaNumber(value)
|
||||
) : key === "chckYmd" ? (
|
||||
<>{formatYyyymmdd(value)}</>
|
||||
) : (
|
||||
<>{value}</>
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
}
|
||||
@@ -173,7 +172,11 @@ export const chRowspanTable = (
|
||||
key={key}
|
||||
className="text-center whitespace-normal break-words"
|
||||
onClick={() => handleCellClick(rowIndex, head.id)}
|
||||
style={{ width: head.widthPercent ? `${head.widthPercent}%` : undefined }}
|
||||
style={{
|
||||
width: head.widthPercent
|
||||
? `${head.widthPercent}%`
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{head.edit === true ? (
|
||||
[
|
||||
@@ -187,15 +190,18 @@ export const chRowspanTable = (
|
||||
type={head.type}
|
||||
onChange={(e) => {
|
||||
handleCommaNumberChange(e, (val) =>
|
||||
handleChange(rowIndex, head.id, val)
|
||||
handleChange(rowIndex, head.id, val),
|
||||
);
|
||||
}}
|
||||
value={formatCommaNumber(value) || ""}
|
||||
disabled={!selectedIds.includes(row.chkgdnSn)}
|
||||
maxLength={numSize}
|
||||
onInput={(e: React.FormEvent<HTMLInputElement>) => {
|
||||
onInput={(e: React.SubmitEvent<HTMLInputElement>) => {
|
||||
const el = e.currentTarget;
|
||||
if (head.type === "number" && el.value.length > numSize) {
|
||||
if (
|
||||
head.type === "number" &&
|
||||
el.value.length > numSize
|
||||
) {
|
||||
el.value = el.value.slice(0, numSize);
|
||||
}
|
||||
}}
|
||||
@@ -226,9 +232,12 @@ export const chRowspanTable = (
|
||||
value={value || ""}
|
||||
disabled={!selectedIds.includes(row.chkgdnSn)}
|
||||
maxLength={numSize}
|
||||
onInput={(e: React.FormEvent<HTMLInputElement>) => {
|
||||
onInput={(e: React.SubmitEvent<HTMLInputElement>) => {
|
||||
const el = e.currentTarget;
|
||||
if (head.type === "number" && el.value.length > numSize) {
|
||||
if (
|
||||
head.type === "number" &&
|
||||
el.value.length > numSize
|
||||
) {
|
||||
el.value = el.value.slice(0, numSize);
|
||||
}
|
||||
}}
|
||||
@@ -237,12 +246,12 @@ export const chRowspanTable = (
|
||||
) : ["smll"].includes(head.id) && head.type === "text" ? (
|
||||
<>-</>
|
||||
) : [
|
||||
"totalSplyUswtrUtztnQnt",
|
||||
"totalRuseFcltUtztnQnt",
|
||||
"splyUswtrUtztnQnt",
|
||||
"ruseFcltUtztnQnt",
|
||||
"levyAmt",
|
||||
].includes(head.id) ? (
|
||||
"totalSplyUswtrUtztnQnt",
|
||||
"totalRuseFcltUtztnQnt",
|
||||
"splyUswtrUtztnQnt",
|
||||
"ruseFcltUtztnQnt",
|
||||
"levyAmt",
|
||||
].includes(head.id) ? (
|
||||
<span
|
||||
className={
|
||||
[
|
||||
@@ -281,7 +290,7 @@ export const chRowspanTable = (
|
||||
|
||||
export const headVisibleChange = (
|
||||
headList: TableUpperProps[],
|
||||
isEdit: boolean
|
||||
isEdit: boolean,
|
||||
) => {
|
||||
return headList.forEach((head) => {
|
||||
if (head.edit !== undefined) head.edit = isEdit;
|
||||
@@ -295,7 +304,7 @@ const handleChange = (
|
||||
rowIndex: number,
|
||||
field: any,
|
||||
value: string,
|
||||
setInputData: (data: any) => void
|
||||
setInputData: (data: any) => void,
|
||||
) => {
|
||||
if (Number(value) < 0 || !Number(value)) {
|
||||
value = "0";
|
||||
@@ -309,17 +318,16 @@ const handleChange = (
|
||||
};
|
||||
}
|
||||
return row;
|
||||
})
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// 재이용실적등록 > 지도점검결과 등록 - 상세 modal의 body
|
||||
export const InputTableBody = (
|
||||
chgRes: any[] | [],
|
||||
headList: TableUpperProps[],
|
||||
setInputData: (data: any[]) => void,
|
||||
isEditable: boolean = true
|
||||
isEditable: boolean = true,
|
||||
) => {
|
||||
// mergeCell 처리용
|
||||
const getRowSpan = (data: any[], key: string, rowIndex: number) => {
|
||||
@@ -360,13 +368,17 @@ export const InputTableBody = (
|
||||
// </TableCell>
|
||||
// );
|
||||
// }
|
||||
const isTextColumn = head.id === "구분" || head.id === "utztnUsgCdText";
|
||||
const isTextColumn =
|
||||
head.id === "구분" || head.id === "utztnUsgCdText";
|
||||
const textAlignClass = isTextColumn
|
||||
? "text-center"
|
||||
: "text-right";
|
||||
|
||||
// 총합계 행이면 "구분"에서 colSpan=2로 병합
|
||||
if ( (data["구분"] === 0 || data["용수명"] === 0) && head.id === "구분" ) {
|
||||
if (
|
||||
(data["구분"] === 0 || data["용수명"] === 0) &&
|
||||
head.id === "구분"
|
||||
) {
|
||||
return (
|
||||
<TableCell
|
||||
key={head.id + headIndex}
|
||||
@@ -378,7 +390,10 @@ export const InputTableBody = (
|
||||
);
|
||||
}
|
||||
// 총합계(합계) 행이면 "용수명" 컬럼은 렌더링하지 않음
|
||||
if ( (data["구분"] === 0 || data["용수명"] === 0) && head.id === "용수명" ) {
|
||||
if (
|
||||
(data["구분"] === 0 || data["용수명"] === 0) &&
|
||||
head.id === "용수명"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -427,8 +442,8 @@ export const InputTableBody = (
|
||||
dataIndex,
|
||||
head.id,
|
||||
String(val),
|
||||
setInputData
|
||||
)
|
||||
setInputData,
|
||||
),
|
||||
)
|
||||
}
|
||||
value={formatCommaNumber(data[head.id as keyof any]) ?? ""}
|
||||
@@ -470,7 +485,7 @@ export const pbnstTableBody = (
|
||||
chgRes: any[] | [],
|
||||
headList: TableUpperProps[],
|
||||
setInputData: (data: any[]) => void,
|
||||
isEditable: boolean = true
|
||||
isEditable: boolean = true,
|
||||
) => {
|
||||
// mergeCell 처리용
|
||||
const getRowSpan = (data: any[], key: string, rowIndex: number) => {
|
||||
@@ -584,8 +599,8 @@ export const pbnstTableBody = (
|
||||
dataIndex,
|
||||
head.id,
|
||||
String(val),
|
||||
setInputData
|
||||
)
|
||||
setInputData,
|
||||
),
|
||||
)
|
||||
}
|
||||
value={formatCommaNumber(data[head.id as keyof any]) ?? ""}
|
||||
@@ -626,4 +641,4 @@ export const pbnstTableBody = (
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,7 +14,9 @@ interface SignupModalProps {
|
||||
}
|
||||
|
||||
function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
const [currentStep, setCurrentStep] = useState<'agreement' | 'signup'>('agreement');
|
||||
const [currentStep, setCurrentStep] = useState<"agreement" | "signup">(
|
||||
"agreement",
|
||||
);
|
||||
const [agreed, setAgreed] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
userId: "",
|
||||
@@ -47,7 +49,7 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
// 모달이 닫힐 때 초기화
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setCurrentStep('agreement');
|
||||
setCurrentStep("agreement");
|
||||
setAgreed(false);
|
||||
setFormData({
|
||||
userId: "",
|
||||
@@ -81,17 +83,19 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
|
||||
const handleNextStep = () => {
|
||||
if (agreed) {
|
||||
setCurrentStep('signup');
|
||||
setCurrentStep("signup");
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
|
||||
// 에러 초기화
|
||||
if (errors[name]) {
|
||||
setErrors(prev => ({ ...prev, [name]: "" }));
|
||||
setErrors((prev) => ({ ...prev, [name]: "" }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -102,20 +106,27 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
if (!formData.userId) newErrors.userId = "아이디를 입력해주세요.";
|
||||
if (!formData.userName) newErrors.userName = "이름을 입력해주세요.";
|
||||
if (!formData.password) newErrors.password = "비밀번호를 입력해주세요.";
|
||||
if (!formData.confirmPassword) newErrors.confirmPassword = "비밀번호 확인을 입력해주세요.";
|
||||
if (!formData.confirmPassword)
|
||||
newErrors.confirmPassword = "비밀번호 확인을 입력해주세요.";
|
||||
if (!formData.pswdHnt) newErrors.pswdHnt = "비밀번호 힌트를 입력해주세요.";
|
||||
if (!formData.laeSeCd) newErrors.laeSeCd = "법인구분을 선택해주세요.";
|
||||
if (!formData.ogdpSeCd) newErrors.ogdpSeCd = "기관구분을 선택해주세요.";
|
||||
|
||||
// 비밀번호 확인
|
||||
if (formData.password && formData.confirmPassword && formData.password !== formData.confirmPassword) {
|
||||
if (
|
||||
formData.password &&
|
||||
formData.confirmPassword &&
|
||||
formData.password !== formData.confirmPassword
|
||||
) {
|
||||
newErrors.confirmPassword = "비밀번호가 일치하지 않습니다.";
|
||||
}
|
||||
|
||||
// 비밀번호 정규식 검증
|
||||
const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/;
|
||||
const passwordRegex =
|
||||
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/;
|
||||
if (formData.password && !passwordRegex.test(formData.password)) {
|
||||
newErrors.password = "비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.";
|
||||
newErrors.password =
|
||||
"비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.";
|
||||
}
|
||||
|
||||
// 이메일 형식 검증
|
||||
@@ -124,8 +135,10 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
}
|
||||
|
||||
// 중복 체크 검증
|
||||
if (idAvailable === false) newErrors.userId = "이미 사용 중인 아이디입니다.";
|
||||
if (emailAvailable === false) newErrors.email = "이미 사용 중인 이메일입니다.";
|
||||
if (idAvailable === false)
|
||||
newErrors.userId = "이미 사용 중인 아이디입니다.";
|
||||
if (emailAvailable === false)
|
||||
newErrors.email = "이미 사용 중인 이메일입니다.";
|
||||
|
||||
setErrors(newErrors);
|
||||
const isValid = Object.keys(newErrors).length === 0;
|
||||
@@ -135,7 +148,7 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
|
||||
const checkUserIdDuplicate = async () => {
|
||||
if (!formData.userId) {
|
||||
setErrors(prev => ({ ...prev, userId: "아이디를 입력해주세요." }));
|
||||
setErrors((prev) => ({ ...prev, userId: "아이디를 입력해주세요." }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -147,19 +160,25 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
});
|
||||
const response = await signupClient.post<boolean>(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/existsUserId`,
|
||||
{ userId: formData.userId }
|
||||
{ userId: formData.userId },
|
||||
);
|
||||
|
||||
const exists = response.data;
|
||||
setIdAvailable(!exists);
|
||||
if (exists) {
|
||||
setErrors(prev => ({ ...prev, userId: "이미 사용 중인 아이디입니다." }));
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
userId: "이미 사용 중인 아이디입니다.",
|
||||
}));
|
||||
} else {
|
||||
setErrors(prev => ({ ...prev, userId: "" }));
|
||||
setErrors((prev) => ({ ...prev, userId: "" }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("아이디 중복 체크 오류:", error);
|
||||
setErrors(prev => ({ ...prev, userId: "아이디 중복 체크 중 오류가 발생했습니다." }));
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
userId: "아이디 중복 체크 중 오류가 발생했습니다.",
|
||||
}));
|
||||
} finally {
|
||||
setIsCheckingId(false);
|
||||
}
|
||||
@@ -167,7 +186,7 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
|
||||
const checkEmailDuplicate = async () => {
|
||||
if (!formData.email) {
|
||||
setErrors(prev => ({ ...prev, email: "이메일을 입력해주세요." }));
|
||||
setErrors((prev) => ({ ...prev, email: "이메일을 입력해주세요." }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -179,28 +198,34 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
});
|
||||
const response = await signupClient.post<boolean>(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/exists`,
|
||||
{ email: formData.email }
|
||||
{ email: formData.email },
|
||||
);
|
||||
|
||||
const exists = response.data;
|
||||
setEmailAvailable(!exists);
|
||||
if (exists) {
|
||||
setErrors(prev => ({ ...prev, email: "이미 사용 중인 이메일입니다." }));
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
email: "이미 사용 중인 이메일입니다.",
|
||||
}));
|
||||
} else {
|
||||
setErrors(prev => ({ ...prev, email: "" }));
|
||||
setErrors((prev) => ({ ...prev, email: "" }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("이메일 중복 체크 오류:", error);
|
||||
setErrors(prev => ({ ...prev, email: "이메일 중복 체크 중 오류가 발생했습니다." }));
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
email: "이메일 중복 체크 중 오류가 발생했습니다.",
|
||||
}));
|
||||
} finally {
|
||||
setIsCheckingEmail(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
const handleSubmit = async (e: React.SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
console.log("회원가입 버튼 클릭됨");
|
||||
|
||||
|
||||
if (!validateForm()) {
|
||||
console.log("폼 검증 실패");
|
||||
return;
|
||||
@@ -230,19 +255,19 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
|
||||
// SignupClient를 통해 익명사용자 회원가입 API 호출
|
||||
const signupClient = SignupClient.getInstance({
|
||||
baseURL: BASE_URL
|
||||
baseURL: BASE_URL,
|
||||
});
|
||||
const response = await signupClient.post(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/anonymouse/users/join`,
|
||||
signupData
|
||||
signupData,
|
||||
);
|
||||
|
||||
console.log("회원가입 성공:", response.data);
|
||||
|
||||
|
||||
// 성공 시 모달 닫기 및 콜백 호출
|
||||
onSuccess();
|
||||
onClose();
|
||||
|
||||
|
||||
// 폼 초기화
|
||||
setFormData({
|
||||
userId: "",
|
||||
@@ -267,13 +292,12 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
setErrors({});
|
||||
setIdAvailable(null);
|
||||
setEmailAvailable(null);
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error("회원가입 오류:", error);
|
||||
if (error instanceof Error) {
|
||||
setErrors(prev => ({ ...prev, submit: error.message }));
|
||||
setErrors((prev) => ({ ...prev, submit: error.message }));
|
||||
} else {
|
||||
setErrors(prev => ({ ...prev, submit: "회원가입에 실패했습니다." }));
|
||||
setErrors((prev) => ({ ...prev, submit: "회원가입에 실패했습니다." }));
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -286,36 +310,52 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h2>{currentStep === 'agreement' ? '개인정보제공동의' : '회원가입'}</h2>
|
||||
<button className="close-button" onClick={onClose}>×</button>
|
||||
<h2>
|
||||
{currentStep === "agreement" ? "개인정보제공동의" : "회원가입"}
|
||||
</h2>
|
||||
<button className="close-button" onClick={onClose}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{currentStep === 'agreement' ? (
|
||||
|
||||
{currentStep === "agreement" ? (
|
||||
<div className="agreement-content">
|
||||
<div className="agreement-text">
|
||||
<h3>개인정보 수집 및 이용 동의</h3>
|
||||
<p>회원가입을 위해 다음과 같이 개인정보를 수집 및 이용합니다.</p>
|
||||
|
||||
|
||||
<h4>1. 수집하는 개인정보 항목</h4>
|
||||
<ul>
|
||||
<li>필수항목: 아이디, 이름, 비밀번호, 비밀번호 힌트, 법인구분, 기관구분</li>
|
||||
<li>선택항목: 이메일, 휴대폰번호, 전화번호, 주소, 기관코드, 상세기관코드, 사업자등록번호</li>
|
||||
<li>
|
||||
필수항목: 아이디, 이름, 비밀번호, 비밀번호 힌트, 법인구분,
|
||||
기관구분
|
||||
</li>
|
||||
<li>
|
||||
선택항목: 이메일, 휴대폰번호, 전화번호, 주소, 기관코드,
|
||||
상세기관코드, 사업자등록번호
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<h4>2. 개인정보의 수집 및 이용목적</h4>
|
||||
<ul>
|
||||
<li>회원가입 및 관리</li>
|
||||
<li>서비스 제공 및 운영</li>
|
||||
<li>고객상담 및 문의응답</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<h4>3. 개인정보의 보유 및 이용기간</h4>
|
||||
<p>회원탈퇴 시까지 (단, 관련 법령에 따라 보존이 필요한 경우 해당 기간까지)</p>
|
||||
|
||||
<p>
|
||||
회원탈퇴 시까지 (단, 관련 법령에 따라 보존이 필요한 경우 해당
|
||||
기간까지)
|
||||
</p>
|
||||
|
||||
<h4>4. 동의 거부권 및 거부 시 불이익</h4>
|
||||
<p>개인정보 수집 및 이용에 대한 동의를 거부할 수 있으며, 동의 거부 시 회원가입이 제한됩니다.</p>
|
||||
<p>
|
||||
개인정보 수집 및 이용에 대한 동의를 거부할 수 있으며, 동의 거부
|
||||
시 회원가입이 제한됩니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="agreement-checkbox">
|
||||
<label>
|
||||
<input
|
||||
@@ -326,13 +366,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
<span>개인정보 수집 및 이용에 동의합니다.</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="agreement-actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="cancel-button"
|
||||
>
|
||||
<button type="button" onClick={onClose} className="cancel-button">
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
@@ -349,7 +385,7 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
<form onSubmit={handleSubmit} className="signup-form">
|
||||
<div className="form-section">
|
||||
<h3>기본 정보</h3>
|
||||
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="userId">아이디 *</label>
|
||||
@@ -373,8 +409,14 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
{isCheckingId ? "확인중..." : "중복확인"}
|
||||
</button>
|
||||
</div>
|
||||
{idAvailable === true && <span className="success-message">사용 가능한 아이디입니다.</span>}
|
||||
{errors.userId && <span className="error-message">{errors.userId}</span>}
|
||||
{idAvailable === true && (
|
||||
<span className="success-message">
|
||||
사용 가능한 아이디입니다.
|
||||
</span>
|
||||
)}
|
||||
{errors.userId && (
|
||||
<span className="error-message">{errors.userId}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -390,7 +432,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
placeholder="이름을 입력하세요"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.userName && <span className="error-message">{errors.userName}</span>}
|
||||
{errors.userName && (
|
||||
<span className="error-message">{errors.userName}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -416,8 +460,14 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
{isCheckingEmail ? "확인중..." : "중복확인"}
|
||||
</button>
|
||||
</div>
|
||||
{emailAvailable === true && <span className="success-message">사용 가능한 이메일입니다.</span>}
|
||||
{errors.email && <span className="error-message">{errors.email}</span>}
|
||||
{emailAvailable === true && (
|
||||
<span className="success-message">
|
||||
사용 가능한 이메일입니다.
|
||||
</span>
|
||||
)}
|
||||
{errors.email && (
|
||||
<span className="error-message">{errors.email}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -434,7 +484,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={20}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.password && <span className="error-message">{errors.password}</span>}
|
||||
{errors.password && (
|
||||
<span className="error-message">{errors.password}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -451,7 +503,11 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={20}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.confirmPassword && <span className="error-message">{errors.confirmPassword}</span>}
|
||||
{errors.confirmPassword && (
|
||||
<span className="error-message">
|
||||
{errors.confirmPassword}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -468,7 +524,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={10}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.pswdHnt && <span className="error-message">{errors.pswdHnt}</span>}
|
||||
{errors.pswdHnt && (
|
||||
<span className="error-message">{errors.pswdHnt}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -485,14 +543,16 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={100}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.pswdAns && <span className="error-message">{errors.pswdAns}</span>}
|
||||
{errors.pswdAns && (
|
||||
<span className="error-message">{errors.pswdAns}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<h3>연락처 정보</h3>
|
||||
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="mphn">휴대폰 번호</label>
|
||||
@@ -506,7 +566,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={11}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.mphn && <span className="error-message">{errors.mphn}</span>}
|
||||
{errors.mphn && (
|
||||
<span className="error-message">{errors.mphn}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -523,14 +585,16 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={11}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.telno && <span className="error-message">{errors.telno}</span>}
|
||||
{errors.telno && (
|
||||
<span className="error-message">{errors.telno}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<h3>주소 정보</h3>
|
||||
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="zip">우편번호</label>
|
||||
@@ -544,7 +608,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={5}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.zip && <span className="error-message">{errors.zip}</span>}
|
||||
{errors.zip && (
|
||||
<span className="error-message">{errors.zip}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -561,7 +627,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={200}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.addr && <span className="error-message">{errors.addr}</span>}
|
||||
{errors.addr && (
|
||||
<span className="error-message">{errors.addr}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -578,14 +646,16 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={200}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.daddr && <span className="error-message">{errors.daddr}</span>}
|
||||
{errors.daddr && (
|
||||
<span className="error-message">{errors.daddr}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<h3>기관 정보</h3>
|
||||
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="laeSeCd">법인구분 *</label>
|
||||
@@ -601,7 +671,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
<option value="02">민간기업</option>
|
||||
<option value="03">개인</option>
|
||||
</select>
|
||||
{errors.laeSeCd && <span className="error-message">{errors.laeSeCd}</span>}
|
||||
{errors.laeSeCd && (
|
||||
<span className="error-message">{errors.laeSeCd}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -621,7 +693,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
<option value="03">공공기관</option>
|
||||
<option value="04">민간기업</option>
|
||||
</select>
|
||||
{errors.ogdpSeCd && <span className="error-message">{errors.ogdpSeCd}</span>}
|
||||
{errors.ogdpSeCd && (
|
||||
<span className="error-message">{errors.ogdpSeCd}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -638,7 +712,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={10}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.instSeCd && <span className="error-message">{errors.instSeCd}</span>}
|
||||
{errors.instSeCd && (
|
||||
<span className="error-message">{errors.instSeCd}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -655,21 +731,25 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
maxLength={10}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.dtlInstCd && <span className="error-message">{errors.dtlInstCd}</span>}
|
||||
{errors.dtlInstCd && (
|
||||
<span className="error-message">{errors.dtlInstCd}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{errors.submit && (
|
||||
<div className="form-row">
|
||||
<div className="error-message submit-error">{errors.submit}</div>
|
||||
<div className="error-message submit-error">
|
||||
{errors.submit}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCurrentStep('agreement')}
|
||||
onClick={() => setCurrentStep("agreement")}
|
||||
className="back-button"
|
||||
disabled={isLoading}
|
||||
>
|
||||
|
||||
@@ -305,7 +305,7 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
const handleSubmit = async (e: React.SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
console.log("회원가입 버튼 클릭됨 - handleSubmit");
|
||||
console.log("현재 폼 데이터:", formData);
|
||||
|
||||
@@ -7,8 +7,10 @@ import "./login.css";
|
||||
|
||||
function LoginPage() {
|
||||
const hostname = window.location.hostname;
|
||||
if (hostname.includes("hasuon.or.kr")) window.location.href = process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL||'/auth/login';
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
if (hostname.includes("hasuon.or.kr"))
|
||||
window.location.href =
|
||||
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
|
||||
const handleSubmit = async (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
@@ -129,4 +131,3 @@ function LoginPage() {
|
||||
}
|
||||
|
||||
export { LoginPage };
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
||||
import "./input-date-picker.css";
|
||||
|
||||
export interface InputDatePickerProps {
|
||||
value?: Date;
|
||||
value?: Date | null;
|
||||
defaultValue?: Date;
|
||||
onChange?: (date: Date | undefined) => void;
|
||||
placeholder?: string;
|
||||
@@ -34,7 +34,7 @@ export function InputDatePicker({
|
||||
const [internalDate, setInternalDate] = useState<Date | undefined>(
|
||||
defaultValue,
|
||||
);
|
||||
const [month, setMonth] = useState<Date | undefined>(
|
||||
const [month, setMonth] = useState<Date | null | undefined>(
|
||||
isControlled ? controlledValue : defaultValue,
|
||||
);
|
||||
|
||||
@@ -112,9 +112,9 @@ export function InputDatePicker({
|
||||
>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
selected={date ?? undefined}
|
||||
captionLayout="dropdown"
|
||||
month={month}
|
||||
month={month ?? undefined}
|
||||
onMonthChange={setMonth}
|
||||
onSelect={(newDate) => {
|
||||
handleDateChange(newDate);
|
||||
|
||||
Reference in New Issue
Block a user