diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 065918c88..6d405c2f8 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -24,7 +24,9 @@ jobs: run: | ssh -i ~/.ssh/deploy_key -p 28 onasys@192.168.0.4 << 'EOF' cd ~/frontend - git pull origin main + git restore . + git clean -fd + git pull origin deploy pnpm install pnpm turbo build pm2 reload all diff --git a/apps/_template/features/README.md b/apps/_template/features/README.md index 2be9a3e17..329175c45 100644 --- a/apps/_template/features/README.md +++ b/apps/_template/features/README.md @@ -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` 레이어 diff --git a/apps/cm/src/pages/auth/login/ui/SignupModal.tsx b/apps/cm/src/pages/auth/login/ui/SignupModal.tsx index d61949ccc..11d7d3544 100644 --- a/apps/cm/src/pages/auth/login/ui/SignupModal.tsx +++ b/apps/cm/src/pages/auth/login/ui/SignupModal.tsx @@ -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) => { + const handleInputChange = ( + e: React.ChangeEvent, + ) => { 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( `${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( `${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) {
e.stopPropagation()}>
-

{currentStep === 'agreement' ? '개인정보제공동의' : '회원가입'}

- +

+ {currentStep === "agreement" ? "개인정보제공동의" : "회원가입"} +

+
- - {currentStep === 'agreement' ? ( + + {currentStep === "agreement" ? (

개인정보 수집 및 이용 동의

회원가입을 위해 다음과 같이 개인정보를 수집 및 이용합니다.

- +

1. 수집하는 개인정보 항목

    -
  • 필수항목: 아이디, 이름, 비밀번호, 비밀번호 힌트, 법인구분, 기관구분
  • -
  • 선택항목: 이메일, 휴대폰번호, 전화번호, 주소, 기관코드, 상세기관코드, 사업자등록번호
  • +
  • + 필수항목: 아이디, 이름, 비밀번호, 비밀번호 힌트, 법인구분, + 기관구분 +
  • +
  • + 선택항목: 이메일, 휴대폰번호, 전화번호, 주소, 기관코드, + 상세기관코드, 사업자등록번호 +
- +

2. 개인정보의 수집 및 이용목적

  • 회원가입 및 관리
  • 서비스 제공 및 운영
  • 고객상담 및 문의응답
- +

3. 개인정보의 보유 및 이용기간

-

회원탈퇴 시까지 (단, 관련 법령에 따라 보존이 필요한 경우 해당 기간까지)

- +

+ 회원탈퇴 시까지 (단, 관련 법령에 따라 보존이 필요한 경우 해당 + 기간까지) +

+

4. 동의 거부권 및 거부 시 불이익

-

개인정보 수집 및 이용에 대한 동의를 거부할 수 있으며, 동의 거부 시 회원가입이 제한됩니다.

+

+ 개인정보 수집 및 이용에 대한 동의를 거부할 수 있으며, 동의 거부 + 시 회원가입이 제한됩니다. +

- +
- +
-
- {idAvailable === true && 사용 가능한 아이디입니다.} - {errors.userId && {errors.userId}} + {idAvailable === true && ( + + 사용 가능한 아이디입니다. + + )} + {errors.userId && ( + {errors.userId} + )}
@@ -390,7 +432,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { placeholder="이름을 입력하세요" disabled={isLoading} /> - {errors.userName && {errors.userName}} + {errors.userName && ( + {errors.userName} + )}
@@ -416,8 +460,14 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { {isCheckingEmail ? "확인중..." : "중복확인"} - {emailAvailable === true && 사용 가능한 이메일입니다.} - {errors.email && {errors.email}} + {emailAvailable === true && ( + + 사용 가능한 이메일입니다. + + )} + {errors.email && ( + {errors.email} + )} @@ -434,7 +484,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={20} disabled={isLoading} /> - {errors.password && {errors.password}} + {errors.password && ( + {errors.password} + )} @@ -451,7 +503,11 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={20} disabled={isLoading} /> - {errors.confirmPassword && {errors.confirmPassword}} + {errors.confirmPassword && ( + + {errors.confirmPassword} + + )} @@ -468,7 +524,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={10} disabled={isLoading} /> - {errors.pswdHnt && {errors.pswdHnt}} + {errors.pswdHnt && ( + {errors.pswdHnt} + )} @@ -485,14 +543,16 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={100} disabled={isLoading} /> - {errors.pswdAns && {errors.pswdAns}} + {errors.pswdAns && ( + {errors.pswdAns} + )}

연락처 정보

- +
@@ -506,7 +566,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={11} disabled={isLoading} /> - {errors.mphn && {errors.mphn}} + {errors.mphn && ( + {errors.mphn} + )}
@@ -523,14 +585,16 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={11} disabled={isLoading} /> - {errors.telno && {errors.telno}} + {errors.telno && ( + {errors.telno} + )}

주소 정보

- +
@@ -544,7 +608,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={5} disabled={isLoading} /> - {errors.zip && {errors.zip}} + {errors.zip && ( + {errors.zip} + )}
@@ -561,7 +627,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={200} disabled={isLoading} /> - {errors.addr && {errors.addr}} + {errors.addr && ( + {errors.addr} + )}
@@ -578,14 +646,16 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={200} disabled={isLoading} /> - {errors.daddr && {errors.daddr}} + {errors.daddr && ( + {errors.daddr} + )}

기관 정보

- +
@@ -601,7 +671,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { - {errors.laeSeCd && {errors.laeSeCd}} + {errors.laeSeCd && ( + {errors.laeSeCd} + )}
@@ -621,7 +693,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { - {errors.ogdpSeCd && {errors.ogdpSeCd}} + {errors.ogdpSeCd && ( + {errors.ogdpSeCd} + )}
@@ -638,7 +712,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={10} disabled={isLoading} /> - {errors.instSeCd && {errors.instSeCd}} + {errors.instSeCd && ( + {errors.instSeCd} + )} @@ -655,21 +731,25 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={10} disabled={isLoading} /> - {errors.dtlInstCd && {errors.dtlInstCd}} + {errors.dtlInstCd && ( + {errors.dtlInstCd} + )} {errors.submit && (
-
{errors.submit}
+
+ {errors.submit} +
)}
- {error && ( -
+
{error}
)} {successMessage && ( -
+
{successMessage}
)} -
- 계정이 없으신가요? +
+ + 계정이 없으신가요?{" "} + - - +
+ + setUserNm(e.target.value)} + /> + + +
diff --git a/apps/cs/src/pages/groups/ui/groups-section.tsx b/apps/cs/src/pages/groups/ui/groups-section.tsx index 9c7f5b6ca..019a4a17f 100644 --- a/apps/cs/src/pages/groups/ui/groups-section.tsx +++ b/apps/cs/src/pages/groups/ui/groups-section.tsx @@ -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({ 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) => { + const handleSubmit = (e: React.SubmitEvent) => { 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 (
-
- - setGroupNm(e.target.value)} - /> - - - - - setSearchParams({ - ...searchParams, - delYn: e.target.checked ? "Y" : "N", - }) - } - /> -
+
+ + setGroupNm(e.target.value)} + /> + + + + + setSearchParams({ + ...searchParams, + delYn: e.target.checked ? "Y" : "N", + }) + } + /> +
diff --git a/apps/cs/src/pages/groups/ui/menu-authority-dialog.tsx b/apps/cs/src/pages/groups/ui/menu-authority-dialog.tsx index cde3cf641..02ca5739f 100644 --- a/apps/cs/src/pages/groups/ui/menu-authority-dialog.tsx +++ b/apps/cs/src/pages/groups/ui/menu-authority-dialog.tsx @@ -95,7 +95,7 @@ function MenuAuthorityDialog({ groupId }: MenuAuthorityDialogProps) { } }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); const keyword = formData.get("keyword") as string; diff --git a/apps/cs/src/pages/image/ui/image-page.tsx b/apps/cs/src/pages/image/ui/image-page.tsx index 6208f180b..212726c5d 100644 --- a/apps/cs/src/pages/image/ui/image-page.tsx +++ b/apps/cs/src/pages/image/ui/image-page.tsx @@ -46,7 +46,7 @@ function ImagePage() { }), }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, diff --git a/apps/cs/src/pages/link/ui/link-page.tsx b/apps/cs/src/pages/link/ui/link-page.tsx index c1b1052fe..a59b9e751 100644 --- a/apps/cs/src/pages/link/ui/link-page.tsx +++ b/apps/cs/src/pages/link/ui/link-page.tsx @@ -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(''); - const [linkNm, setLinkNm] = useState(''); - const [linkClfsCd, setLinkClfsCd] = useState(''); + const [linkSn, setLinkSn] = useState(""); + const [linkNm, setLinkNm] = useState(""); + const [linkClfsCd, setLinkClfsCd] = useState(""); - const [linkSearchParam, setLinkSearchParam] = useState({ - linkSn: '', - linkNm: '', - linkClfsCd: '', + const [linkSearchParam, setLinkSearchParam] = useState({ + 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) => { + 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) => { - 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 ( -
-
-
- - setLinkSn(e.target.value)} - autoComplete="off" - /> - - setLinkNm(e.target.value)} - autoComplete="off" - /> - - - - -
-
-
-
-

연계 목록

- -
- - -
-
-
- {!query.isLoading ? -
{tableComponent}
- : -
-
- -
-
- } -
-
+ const handleClickRegist = () => { + router.push("link/register"); + }; + + const { tableComponent } = useDataTable({ + query, + tableOptions: { + columns, + manualPagination: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + }, + dataTableOptions: { + onRowClick: handleRowClick, + viewPagination: true, + }, + }); + + return ( +
+
+
+ + setLinkSn(e.target.value)} + autoComplete="off" + /> + + setLinkNm(e.target.value)} + autoComplete="off" + /> + + + + +
+
+
+
+

연계 목록

+ +
+ + +
- ); +
+ {!query.isLoading ? ( +
{tableComponent}
+ ) : ( +
+
+ +
+
+ )} +
+
+
+ ); } -export { LinkPage }; \ No newline at end of file +export { LinkPage }; diff --git a/apps/cs/src/pages/linkLog/ui/link-log-page.tsx b/apps/cs/src/pages/linkLog/ui/link-log-page.tsx index dd39a4726..5a0637b72 100644 --- a/apps/cs/src/pages/linkLog/ui/link-log-page.tsx +++ b/apps/cs/src/pages/linkLog/ui/link-log-page.tsx @@ -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(''); - const [linkNm, setLinkNm] = useState(''); - const [isSucc, setIsSucc] = useState('ALL'); - const [startDate, setStartDate] = useState(dayjs().subtract(7, "day").format('YYYY-MM-DD')); - const [endDate, setEndDate] = useState(dayjs().format('YYYY-MM-DD')); + const [linkSn, setLinkSn] = useState(""); + const [linkNm, setLinkNm] = useState(""); + const [isSucc, setIsSucc] = useState("ALL"); + const [startDate, setStartDate] = useState( + dayjs().subtract(7, "day").format("YYYY-MM-DD"), + ); + const [endDate, setEndDate] = useState(dayjs().format("YYYY-MM-DD")); - const [linkSearchParam, setLinkSearchParam] = useState({ - linkSn: '', - linkNm: '', - isSucc: '', - startDate: '', - endDate: '', + const [linkSearchParam, setLinkSearchParam] = useState({ + linkSn: "", + linkNm: "", + isSucc: "", + startDate: "", + endDate: "", + }); + + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 100, + }); + + const [rowSelection, setRowSelection] = useState({}); + + 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({ - pageIndex: 0, - pageSize: 100, - }); + const handleSubmit = async (e?: React.SubmitEvent) => { + e?.preventDefault(); - const [rowSelection, setRowSelection] = useState({}); - - 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) => { - 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 ( -
-
-
- - setLinkSn(e.target.value)} - autoComplete="off" - /> - - setLinkNm(e.target.value)} - autoComplete="off" - /> - - - - setStartDate(dayjs(e).format('YYYY-MM-DD'))} /> - - setEndDate(dayjs(e).format('YYYY-MM-DD'))} /> - - - -
-
-
-

로그 목록 (검색 조건에 따른 건수 : {query.data?.totalElements.toLocaleString('ko-KR') ?? 0} )

-
-
- {query.isLoading ? -
-
- -
-
- : -
{tableComponent}
- } -
-
+ return ( +
+
+
+ + setLinkSn(e.target.value)} + autoComplete="off" + /> + + setLinkNm(e.target.value)} + autoComplete="off" + /> + + + + setStartDate(dayjs(e).format("YYYY-MM-DD"))} + /> + + setEndDate(dayjs(e).format("YYYY-MM-DD"))} + /> + + + +
+
+
+

+ 로그 목록 (검색 조건에 따른 건수 :{" "} + {query.data?.totalElements.toLocaleString("ko-KR") ?? 0} ) +

- ) -}; +
+ {query.isLoading ? ( +
+
+ +
+
+ ) : ( +
{tableComponent}
+ )} +
+
+
+ ); +} -export { LinkLogPage }; \ No newline at end of file +export { LinkLogPage }; diff --git a/apps/cs/src/pages/linkParams/ui/link-params-page.tsx b/apps/cs/src/pages/linkParams/ui/link-params-page.tsx index 319df80b2..001eb9953 100644 --- a/apps/cs/src/pages/linkParams/ui/link-params-page.tsx +++ b/apps/cs/src/pages/linkParams/ui/link-params-page.tsx @@ -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(''); - const [linkNm, setLinkNm] = useState(''); + const [linkSn, setLinkSn] = useState(""); + const [linkNm, setLinkNm] = useState(""); - const [linkSearchParam, setLinkSearchParam] = useState({ - linkSn: '', - linkNm: '' + const [linkSearchParam, setLinkSearchParam] = useState({ + linkSn: "", + linkNm: "", + }); + + const [selectedLink, setSelectedLink] = useState<{ + linkSn: string; + linkNm: string; + }>({ + linkSn: "", + linkNm: "", + }); + + const [rowSelection, setRowSelection] = useState({}); + + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 100, + }); + + const { confirm } = useConfirm(); + + const [selectedVrblIds, setSelectedVrblIds] = useState([]); + const [isDBdata, setIsDBData] = useState(false); + const [dialogOpen, setDialogOpen] = useState(false); + const [onConfirm, setOnConfirm] = useState(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) => { + 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({ + resolver: zodResolver(linkVrblFormSchema), + defaultValues: {}, + }); + + const handleRowClick = async (link: Link) => { + setSelectedLink({ + linkSn: link.link_sn, + linkNm: link.link_nm, }); - const [rowSelection, setRowSelection] = useState({}); + setSelectedVrblIds([]); - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 100, + if (dbIdList.includes(link.link_clsf_cd)) { + setIsDBData(true); + } else { + setIsDBData(false); + } + }; + + const handleSubmit = (e: React.SubmitEvent) => { + e.preventDefault(); + + setSelectedLink({ + linkSn: "", + linkNm: "", }); - const { confirm } = useConfirm(); - - const [selectedVrblIds, setSelectedVrblIds] = useState([]); - const [isDBdata, setIsDBData] = useState(false); - const [dialogOpen, setDialogOpen] = useState(false); - const [onConfirm, setOnConfirm] = useState(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) => { - 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({ - resolver: zodResolver(linkVrblFormSchema), - defaultValues: {}, - }); + // LinkVrbl 데이터 표시 로직 + const renderLinkVrblContent = () => { + if (isDBdata) { + return ( +
+ 변수를 지원하지 않는 연계 데이터입니다. +
+ ); + } - const handleRowClick = async (link: Link) => { - setSelectedLink({ - linkSn: link.link_sn, - linkNm: link.link_nm, - }); + if (!selectedLink.linkSn) { + return ( +
+ 연계 데이터를 선택해주세요. +
+ ); + } - setSelectedVrblIds([]); - - if (dbIdList.includes(link.link_clsf_cd)) { - setIsDBData(true); - } else { - setIsDBData(false); - } - }; - - const handleSubmit = (e: React.FormEvent) => { - 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 ( -
- 변수를 지원하지 않는 연계 데이터입니다. -
- ); - } - - if (!selectedLink.linkSn) { - return ( -
- 연계 데이터를 선택해주세요. -
- ); - } - - if (linkVrblQuery.isLoading) { - return ( -
-
- -
-
- ); - } - - return ( - <> -
- {tableData.length > 0 && ( - - )} - -
- {tableData.length > 0 ? ( -
- -
- ) : ( -
- 등록된 연계 변수가 없습니다. -
- )} - - ); - }; + if (linkVrblQuery.isLoading) { + return ( +
+
+ +
+
+ ); + } return ( -
-
-
- - setLinkSn(e.target.value)} - autoComplete="off" - /> - - setLinkNm(e.target.value)} - autoComplete="off" - /> - - -
-
-
-
-
-

연계 목록

-
-
- {!query.isLoading ? ( -
{tableComponent}
- ) : ( -
-
- -
-
- )} -
-
-
-
-

연계 변수 정보

-
-
- - - - - - - - 연계 일련번호 - - {selectedLink.linkSn ? selectedLink.linkSn : '-'} - - - - 연계 명 - - {selectedLink.linkNm ? selectedLink.linkNm : '-'} - - - -
- {renderLinkVrblContent()} -
-
-
- + <> +
+ {tableData.length > 0 && ( + + )} +
+ {tableData.length > 0 ? ( +
+ +
+ ) : ( +
+ 등록된 연계 변수가 없습니다. +
+ )} + ); + }; + + return ( +
+
+
+ + setLinkSn(e.target.value)} + autoComplete="off" + /> + + setLinkNm(e.target.value)} + autoComplete="off" + /> + + +
+
+
+
+
+

연계 목록

+
+
+ {!query.isLoading ? ( +
{tableComponent}
+ ) : ( +
+
+ +
+
+ )} +
+
+
+
+

연계 변수 정보

+
+
+ + + + + + + + 연계 일련번호 + + {selectedLink.linkSn ? selectedLink.linkSn : "-"} + + + + 연계 명 + + {selectedLink.linkNm ? selectedLink.linkNm : "-"} + + + +
+ {renderLinkVrblContent()} +
+
+
+ +
+ ); } -export { LinkParamsPage }; \ No newline at end of file +export { LinkParamsPage }; diff --git a/apps/cs/src/pages/linkStats/ui/link-stats-page.tsx b/apps/cs/src/pages/linkStats/ui/link-stats-page.tsx index f6168ba76..19163d24c 100644 --- a/apps/cs/src/pages/linkStats/ui/link-stats-page.tsx +++ b/apps/cs/src/pages/linkStats/ui/link-stats-page.tsx @@ -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 | null>(null); + const failChartRef = useRef | null>(null); - const succChartRef = useRef | null>(null); - const failChartRef = useRef | null>(null); + const [startDate, setStartDate] = useState( + dayjs().subtract(7, "day").format("YYYY-MM-DD"), + ); + const [endDate, setEndDate] = useState(dayjs().format("YYYY-MM-DD")); - const [startDate, setStartDate] = useState(dayjs().subtract(7, "day").format('YYYY-MM-DD')); - const [endDate, setEndDate] = useState(dayjs().format('YYYY-MM-DD')); + const [rowSelection, setRowSelection] = useState({}); - const [rowSelection, setRowSelection] = useState({}); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 100, + }); - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 100, + const [linkStatsList, setLinkStatsList] = + useState(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({ + 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(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({ - linkSn: '', - linkNm: '', - startDate: '', - endDate: '', - }); + const handleSubmit = async (e?: React.SubmitEvent) => { + 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, + ) => { + 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, + ) => { + 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) => { - 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) => { - 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) => { - 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 ( -
-
-
- - setStartDate(dayjs(e).format('YYYY-MM-DD'))} /> - - setEndDate(dayjs(e).format('YYYY-MM-DD'))} /> - - - - -
-
-
-
-

조회 일자 : {`${linkSearchParam.startDate} ~ ${linkSearchParam.endDate}`}

-
-
- -
-
- -
-
-
-
-

- {logQueryParam?.linkNm ? `${logQueryParam.linkNm} 연계 ${logQueryParam.isSucc === 'FAIL' ? '실패' : '성공'} 로그 목록 (${linkSearchParam.startDate} ~ ${linkSearchParam.endDate}) - ${query.data?.totalElements.toLocaleString('ko-KR') ?? 0}건` : '로그 목록'} -

-
-
- {logQueryParam?.linkNm ? - query.isLoading ? -
-
- -
-
- : -
{tableComponent}
- : -
- 차트 데이터를 선택해주세요. -
- } -
-
-
+ return ( +
+
+
+ + setStartDate(dayjs(e).format("YYYY-MM-DD"))} + /> + + setEndDate(dayjs(e).format("YYYY-MM-DD"))} + /> + + + + +
+
+
+
+

+ 조회 일자 :{" "} + {`${linkSearchParam.startDate} ~ ${linkSearchParam.endDate}`} +

+
+
+ +
+
+ +
- ) -}; +
+
+

+ {logQueryParam?.linkNm + ? `${logQueryParam.linkNm} 연계 ${logQueryParam.isSucc === "FAIL" ? "실패" : "성공"} 로그 목록 (${linkSearchParam.startDate} ~ ${linkSearchParam.endDate}) - ${query.data?.totalElements.toLocaleString("ko-KR") ?? 0}건` + : "로그 목록"} +

+
+
+ {logQueryParam?.linkNm ? ( + query.isLoading ? ( +
+
+ +
+
+ ) : ( +
{tableComponent}
+ ) + ) : ( +
+ 차트 데이터를 선택해주세요. +
+ )} +
+
+
+
+ ); +} -export { LinkStatsPage }; \ No newline at end of file +export { LinkStatsPage }; diff --git a/apps/cs/src/pages/linkTable/ui/link-table-page.tsx b/apps/cs/src/pages/linkTable/ui/link-table-page.tsx index 0394e7bf6..3ccee78e8 100644 --- a/apps/cs/src/pages/linkTable/ui/link-table-page.tsx +++ b/apps/cs/src/pages/linkTable/ui/link-table-page.tsx @@ -1,20 +1,35 @@ -'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, type PaginationState, type RowSelectionState, useReactTable } from "@tanstack/react-table"; -import { LinkApi, type Link, type LinkSearchParam, type LinkTable, type LinkTableColumns, type LinkTableColData, type LinkTableFormData, LinkTableColumnsSchema, LinkTableSchema } from "@/entities/link"; +import { + getCoreRowModel, + type PaginationState, + type RowSelectionState, + useReactTable, +} from "@tanstack/react-table"; +import { + LinkApi, + type Link, + type LinkSearchParam, + type LinkTable, + type LinkTableColumns, + type LinkTableColData, + type LinkTableFormData, + LinkTableColumnsSchema, + LinkTableSchema, +} from "@/entities/link"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { columns } from "../../linkParams/model/table-columns"; import { tableColumns } from "../model/table-columns"; @@ -31,680 +46,778 @@ import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form"; import { useDataTable } from "@/shared/lib"; function LinkTablePage() { - const queryClient = useQueryClient(); + const queryClient = useQueryClient(); - const userUniqId = getCookie(AUTH_USER_ID) ?? ""; + const userUniqId = getCookie(AUTH_USER_ID) ?? ""; - const [linkSn, setLinkSn] = useState(''); - const [linkNm, setLinkNm] = useState(''); + const [linkSn, setLinkSn] = useState(""); + const [linkNm, setLinkNm] = useState(""); - const [linkSearchParam, setLinkSearchParam] = useState({ - linkSn: '', - linkNm: '' - }); + const [linkSearchParam, setLinkSearchParam] = useState({ + linkSn: "", + linkNm: "", + }); - const [selectedTableInfo, setSelectedTableInfo] = useState(null); - const [tableColList, setTableColList] = useState([]); - const [dialogOpen, setDialogOpen] = useState(false); - const [selectedColIds, setSelectedColIds] = useState([]); + const [selectedTableInfo, setSelectedTableInfo] = useState( + null, + ); + const [tableColList, setTableColList] = useState([]); + const [dialogOpen, setDialogOpen] = useState(false); + const [selectedColIds, setSelectedColIds] = useState([]); - const [rowSelection, setRowSelection] = useState({}); + const [rowSelection, setRowSelection] = useState({}); - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 100, - }); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 100, + }); - const { setMessage: alert } = useAlertStore((state) => state); + const { setMessage: alert } = useAlertStore((state) => state); - const { confirm } = useConfirm(); - const [onConfirm, setOnConfirm] = useState(false); + const { confirm } = useConfirm(); + const [onConfirm, setOnConfirm] = useState(false); - const handleConfirm = async (callback: () => Promise) => { - setOnConfirm(true); - await callback(); - setOnConfirm(false); - }; + const handleConfirm = async (callback: () => Promise) => { + setOnConfirm(true); + await callback(); + setOnConfirm(false); + }; - const query = useQuery({ - queryKey: ['/api/link/getLinkList', linkSearchParam], - queryFn: () => LinkApi.getLinkList({ - ...linkSearchParam, - page: pagination.pageIndex, - size: pagination.pageSize, - }), - }); + const query = useQuery({ + queryKey: ["/api/link/getLinkList", linkSearchParam], + queryFn: () => + LinkApi.getLinkList({ + ...linkSearchParam, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + }); - const { mutateAsync: insertLinkTableCol } = useMutation({ - mutationFn: async (data: LinkTableColData) => await LinkApi.insertLinkTableCol({ - ...data, - mdfr_id: userUniqId, - }), - onSuccess: async () => { - if (selectedTableInfo) { - await queryClient.invalidateQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`] - }); - await queryClient.invalidateQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`] - }); - - const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn); - const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn); - - setSelectedTableInfo(tableData); - setTableColList(tableColList.content); - setSelectedColIds([]); - setDialogOpen(false); - alert('저장되었습니다.'); - } - } - }); - - const { mutateAsync: updateLinkTableCol } = useMutation({ - mutationFn: async (data: LinkTableColData) => await LinkApi.updateLinkTableCol({ - ...data, - mdfr_id: userUniqId, - }), - onSuccess: async () => { - if (selectedTableInfo) { - await queryClient.invalidateQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`] - }); - await queryClient.invalidateQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`] - }); - - const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn); - const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn); - - setSelectedTableInfo(tableData); - setTableColList(tableColList.content); - setSelectedColIds([]); - setDialogOpen(false); - alert('수정되었습니다.'); - } - } - }); - - const { mutateAsync: deleteLinkTableCol } = useMutation({ - mutationFn: async (data: string[]) => await LinkApi.deleteLinkTableCol(data), - onSuccess: async () => { - if (selectedTableInfo) { - await queryClient.invalidateQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`] - }); - await queryClient.invalidateQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`] - }); - - const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn); - const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn); - - setSelectedTableInfo(tableData); - setTableColList(tableColList.content); - setSelectedColIds([]); - setDialogOpen(false); - alert('삭제되었습니다.'); - } - } - }); - - const { mutateAsync: insertLinkTableInfo } = useMutation({ - mutationFn: async (data: LinkTableFormData) => await LinkApi.insertLinkTableInfo(data), - onSuccess: async () => { - if (selectedTableInfo) { - await queryClient.invalidateQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`] - }); - await queryClient.invalidateQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`] - }); - - const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn); - const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn); - - tableForm.reset({ - link_sn: tableData.link_sn, - link_nm: tableData.link_nm, - tbl_sn: tableData.tbl_sn ?? "", - sou_tbl_nm: tableData.sou_tbl_nm ?? "", - sou_tbl_korn_nm: tableData.sou_tbl_korn_nm ?? "", - sou_tbl_expln: tableData.sou_tbl_expln ?? "", - trsf_tbl_expln: tableData.trsf_tbl_expln ?? "", - trsf_tbl_nm: tableData.trsf_tbl_nm ?? "", - trsf_tbl_korn_nm: tableData.trsf_tbl_korn_nm ?? "", - }); - setSelectedTableInfo(tableData); - setTableColList(tableColList.content); - setSelectedColIds([]); - alert('저장되었습니다.'); - } - } - }); - - const { mutateAsync: updateLinkTableInfo } = useMutation({ - mutationFn: async (data: LinkTableFormData) => await LinkApi.updateLinkTableInfo(data), - onSuccess: async () => { - if (selectedTableInfo) { - await queryClient.invalidateQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`] - }); - await queryClient.invalidateQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`] - }); - - const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn); - const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn); - - setSelectedTableInfo(tableData); - setTableColList(tableColList.content); - setSelectedColIds([]); - alert('수정되었습니다.'); - } - } - }); - - const tableCol = useMemo( - () => - tableColumns({ selectedColIds, setSelectedColIds }), - [selectedColIds] - ); - - const colTable = useReactTable({ - data: tableColList, - columns: tableCol, - getCoreRowModel: getCoreRowModel(), - }); - - const form = useForm({ - resolver: zodResolver(LinkTableColumnsSchema), - defaultValues: { - pk_yn: "N" - } - }); - - const tableForm = useForm({ - resolver: zodResolver(LinkTableSchema), - defaultValues: {}, - }); - - const handleResetSearchParam = async () => { - setSelectedTableInfo(null); - setTableColList([]); - setLinkSn(''); - setLinkNm(''); - setLinkSearchParam({ - ...linkSearchParam, - linkSn: '', - linkNm: '', + const { mutateAsync: insertLinkTableCol } = useMutation({ + mutationFn: async (data: LinkTableColData) => + await LinkApi.insertLinkTableCol({ + ...data, + mdfr_id: userUniqId, + }), + onSuccess: async () => { + if (selectedTableInfo) { + await queryClient.invalidateQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`, + ], }); - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - setSelectedTableInfo(null); - setTableColList([]); - - setLinkSearchParam({ - ...linkSearchParam, - linkSn, - linkNm + await queryClient.invalidateQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`, + ], }); - }; - const handleRowClick = async (data: Link) => { - const tableData = await LinkApi.getLinkTableInfo(data.link_sn); - const tableColList = await LinkApi.getLinkTableColList(data.link_sn); + const tableData = await LinkApi.getLinkTableInfo( + selectedTableInfo.link_sn, + ); + const tableColList = await LinkApi.getLinkTableColList( + selectedTableInfo.link_sn, + ); setSelectedTableInfo(tableData); setTableColList(tableColList.content); setSelectedColIds([]); + setDialogOpen(false); + alert("저장되었습니다."); + } + }, + }); + + const { mutateAsync: updateLinkTableCol } = useMutation({ + mutationFn: async (data: LinkTableColData) => + await LinkApi.updateLinkTableCol({ + ...data, + mdfr_id: userUniqId, + }), + onSuccess: async () => { + if (selectedTableInfo) { + await queryClient.invalidateQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`, + ], + }); + await queryClient.invalidateQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`, + ], + }); + + const tableData = await LinkApi.getLinkTableInfo( + selectedTableInfo.link_sn, + ); + const tableColList = await LinkApi.getLinkTableColList( + selectedTableInfo.link_sn, + ); + + setSelectedTableInfo(tableData); + setTableColList(tableColList.content); + setSelectedColIds([]); + setDialogOpen(false); + alert("수정되었습니다."); + } + }, + }); + + const { mutateAsync: deleteLinkTableCol } = useMutation({ + mutationFn: async (data: string[]) => + await LinkApi.deleteLinkTableCol(data), + onSuccess: async () => { + if (selectedTableInfo) { + await queryClient.invalidateQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`, + ], + }); + await queryClient.invalidateQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`, + ], + }); + + const tableData = await LinkApi.getLinkTableInfo( + selectedTableInfo.link_sn, + ); + const tableColList = await LinkApi.getLinkTableColList( + selectedTableInfo.link_sn, + ); + + setSelectedTableInfo(tableData); + setTableColList(tableColList.content); + setSelectedColIds([]); + setDialogOpen(false); + alert("삭제되었습니다."); + } + }, + }); + + const { mutateAsync: insertLinkTableInfo } = useMutation({ + mutationFn: async (data: LinkTableFormData) => + await LinkApi.insertLinkTableInfo(data), + onSuccess: async () => { + if (selectedTableInfo) { + await queryClient.invalidateQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`, + ], + }); + await queryClient.invalidateQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`, + ], + }); + + const tableData = await LinkApi.getLinkTableInfo( + selectedTableInfo.link_sn, + ); + const tableColList = await LinkApi.getLinkTableColList( + selectedTableInfo.link_sn, + ); tableForm.reset({ - link_sn: tableData.link_sn, - link_nm: tableData.link_nm, - tbl_sn: tableData.tbl_sn ?? "", - sou_tbl_nm: tableData.sou_tbl_nm ?? "", - sou_tbl_korn_nm: tableData.sou_tbl_korn_nm ?? "", - sou_tbl_expln: tableData.sou_tbl_expln ?? "", - trsf_tbl_expln: tableData.trsf_tbl_expln ?? "", - trsf_tbl_nm: tableData.trsf_tbl_nm ?? "", - trsf_tbl_korn_nm: tableData.trsf_tbl_korn_nm ?? "", + link_sn: tableData.link_sn, + link_nm: tableData.link_nm, + tbl_sn: tableData.tbl_sn ?? "", + sou_tbl_nm: tableData.sou_tbl_nm ?? "", + sou_tbl_korn_nm: tableData.sou_tbl_korn_nm ?? "", + sou_tbl_expln: tableData.sou_tbl_expln ?? "", + trsf_tbl_expln: tableData.trsf_tbl_expln ?? "", + trsf_tbl_nm: tableData.trsf_tbl_nm ?? "", + trsf_tbl_korn_nm: tableData.trsf_tbl_korn_nm ?? "", }); - }; + setSelectedTableInfo(tableData); + setTableColList(tableColList.content); + setSelectedColIds([]); + alert("저장되었습니다."); + } + }, + }); - const handleColumnRowClick = async (data: LinkTableColumns) => { - const colInfo = await LinkApi.getLinkTableColInfo({ colSn: data.col_sn, tblSn: data.tbl_sn }); + const { mutateAsync: updateLinkTableInfo } = useMutation({ + mutationFn: async (data: LinkTableFormData) => + await LinkApi.updateLinkTableInfo(data), + onSuccess: async () => { + if (selectedTableInfo) { + await queryClient.invalidateQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`, + ], + }); + await queryClient.invalidateQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`, + ], + }); - if (colInfo) { - let newData = { ...colInfo }; + const tableData = await LinkApi.getLinkTableInfo( + selectedTableInfo.link_sn, + ); + const tableColList = await LinkApi.getLinkTableColList( + selectedTableInfo.link_sn, + ); - if (colInfo.mdfr_id) { - const userName = await fetchUserInfo(); - newData = { - ...colInfo, - mdfr_id: userName, - }; - } + setSelectedTableInfo(tableData); + setTableColList(tableColList.content); + setSelectedColIds([]); + alert("수정되었습니다."); + } + }, + }); - form.reset(newData); + const tableCol = useMemo( + () => tableColumns({ selectedColIds, setSelectedColIds }), + [selectedColIds], + ); - } - setDialogOpen(true) - }; + const colTable = useReactTable({ + data: tableColList, + columns: tableCol, + getCoreRowModel: getCoreRowModel(), + }); - const fetchUserInfo = async () => { - const userData = await getUserInfo(); - return userData.userName; - }; + const form = useForm({ + resolver: zodResolver(LinkTableColumnsSchema), + defaultValues: { + pk_yn: "N", + }, + }); - const handleDeleteParam = async () => { - if (selectedColIds.length === 0) { - alert('삭제할 컬럼을 선택해 주세요.'); - } else { - await handleConfirm(async () => { - if (await confirm('선택한 컬럼 정보를 삭제하시겠습니까?')) { - deleteLinkTableCol(selectedColIds); - } - }); - } - }; + const tableForm = useForm({ + resolver: zodResolver(LinkTableSchema), + defaultValues: {}, + }); - const handleTableSubmit = async (data: LinkTableFormData) => { - const regTblInfo = await LinkApi.countLinkTblNm(data.trsf_tbl_nm); - const confirmMsg = (regTblInfo.count > 0 && data.trsf_tbl_nm !== selectedTableInfo?.trsf_tbl_nm) ? `이미 사용중인 테이블 명입니다. 해당 테이블 명으로 ${!data.tbl_sn ? '저장' : '수정'}하시겠습니까?` : `${!data.tbl_sn ? '저장' : '수정'}하시겠습니까?`; - if (!data.tbl_sn) { - await handleConfirm(async () => { - if (await confirm(confirmMsg)) { - insertLinkTableInfo({ - ...data, - mdfr_id: userUniqId, - tbl_sn: (regTblInfo.count > 0) ? regTblInfo.tbl_sn : "", - }); - } - }); - } else { - await handleConfirm(async () => { - if (await confirm(confirmMsg)) { - updateLinkTableInfo({ - ...data, - tbl_sn: (regTblInfo.count > 0) ? regTblInfo.tbl_sn : "", - mdfr_id: userUniqId, - }); - } - }); - } + const handleResetSearchParam = async () => { + setSelectedTableInfo(null); + setTableColList([]); + setLinkSn(""); + setLinkNm(""); + setLinkSearchParam({ + ...linkSearchParam, + linkSn: "", + linkNm: "", + }); + }; - }; + const handleSubmit = (e: React.SubmitEvent) => { + e.preventDefault(); + setSelectedTableInfo(null); + setTableColList([]); - const { tableComponent } = useDataTable({ - query, - tableOptions: { - columns, - state: { - pagination, - rowSelection, - }, - manualPagination: true, - enableMultiRowSelection: true, - getCoreRowModel: getCoreRowModel(), - onPaginationChange: setPagination, - onRowSelectionChange: setRowSelection, - }, - dataTableOptions: { - onRowClick: handleRowClick, - viewPagination: true, - }, + setLinkSearchParam({ + ...linkSearchParam, + linkSn, + linkNm, + }); + }; + + const handleRowClick = async (data: Link) => { + const tableData = await LinkApi.getLinkTableInfo(data.link_sn); + const tableColList = await LinkApi.getLinkTableColList(data.link_sn); + + setSelectedTableInfo(tableData); + setTableColList(tableColList.content); + setSelectedColIds([]); + + tableForm.reset({ + link_sn: tableData.link_sn, + link_nm: tableData.link_nm, + tbl_sn: tableData.tbl_sn ?? "", + sou_tbl_nm: tableData.sou_tbl_nm ?? "", + sou_tbl_korn_nm: tableData.sou_tbl_korn_nm ?? "", + sou_tbl_expln: tableData.sou_tbl_expln ?? "", + trsf_tbl_expln: tableData.trsf_tbl_expln ?? "", + trsf_tbl_nm: tableData.trsf_tbl_nm ?? "", + trsf_tbl_korn_nm: tableData.trsf_tbl_korn_nm ?? "", + }); + }; + + const handleColumnRowClick = async (data: LinkTableColumns) => { + const colInfo = await LinkApi.getLinkTableColInfo({ + colSn: data.col_sn, + tblSn: data.tbl_sn, }); - return ( -
-
-
- - setLinkSn(e.target.value)} - autoComplete="off" - /> - - setLinkNm(e.target.value)} - autoComplete="off" - /> - - -
-
-
-
-
-

연계 목록

-
-
- {!query.isLoading ? -
{tableComponent}
- : -
-
- -
-
- } -
-
-
-
-

연계 테이블 정보

-
-
-
- - {selectedTableInfo && -
- -
- } - - - - - - - - 연계 일련번호 - - {selectedTableInfo ? - ( - - - - - - )} - /> - : - - - } - - - - 연계 명 - - {selectedTableInfo ? - ( - - - - - - )} - /> - : - - - } - - - - 원천 테이블 명 - - {selectedTableInfo ? - ( - - - - - {tableForm.formState.errors.sou_tbl_nm && ( -

- {tableForm.formState.errors.sou_tbl_nm.message} -

- )} -
- )} - /> - : - - - } -
-
- - 원천 테이블 한글명 - - {selectedTableInfo ? - ( - - - - - {tableForm.formState.errors.sou_tbl_korn_nm && ( -

- {tableForm.formState.errors.sou_tbl_korn_nm.message} -

- )} -
- )} - /> - : - - - } -
-
- - 원천 테이블 설명 - - {selectedTableInfo ? - ( - - - - - {tableForm.formState.errors.sou_tbl_expln && ( -

- {tableForm.formState.errors.sou_tbl_expln.message} -

- )} -
- )} - /> - : - - - } -
-
- - * 변환 테이블 명 - - {selectedTableInfo ? - ( - - - - - {tableForm.formState.errors.trsf_tbl_nm && ( -

- {tableForm.formState.errors.trsf_tbl_nm.message} -

- )} -
- )} - /> - : - - - } -
-
- - * 변환 테이블 한글명 - - {selectedTableInfo ? - ( - - - - - {tableForm.formState.errors.trsf_tbl_korn_nm && ( -

- {tableForm.formState.errors.trsf_tbl_korn_nm.message} -

- )} -
- )} - /> - : - - - } -
-
- - 변환 테이블 설명 - - {selectedTableInfo ? - ( - - - - - {tableForm.formState.errors.trsf_tbl_expln && ( -

- {tableForm.formState.errors.trsf_tbl_expln.message} -

- )} -
- )} - /> - : - - - } -
-
-
-
-
- - {selectedTableInfo ? - <> -
- {tableColList.length > 0 && - - } - {selectedTableInfo.tbl_sn && - - } -
- {selectedTableInfo.tbl_sn ? - (tableColList.length > 0 ? -
- -
- : -
- 등록된 테이블 컬럼이 없습니다. -
) - : -
- 테이블 정보를 등록해주세요. -
- } - - : -
- 연계 데이터를 선택해주세요. -
- } -
-
-
- -
- ) -}; + if (colInfo) { + let newData = { ...colInfo }; -export { LinkTablePage }; \ No newline at end of file + if (colInfo.mdfr_id) { + const userName = await fetchUserInfo(); + newData = { + ...colInfo, + mdfr_id: userName, + }; + } + + form.reset(newData); + } + setDialogOpen(true); + }; + + const fetchUserInfo = async () => { + const userData = await getUserInfo(); + return userData.userName; + }; + + const handleDeleteParam = async () => { + if (selectedColIds.length === 0) { + alert("삭제할 컬럼을 선택해 주세요."); + } else { + await handleConfirm(async () => { + if (await confirm("선택한 컬럼 정보를 삭제하시겠습니까?")) { + deleteLinkTableCol(selectedColIds); + } + }); + } + }; + + const handleTableSubmit = async (data: LinkTableFormData) => { + const regTblInfo = await LinkApi.countLinkTblNm(data.trsf_tbl_nm); + const confirmMsg = + regTblInfo.count > 0 && + data.trsf_tbl_nm !== selectedTableInfo?.trsf_tbl_nm + ? `이미 사용중인 테이블 명입니다. 해당 테이블 명으로 ${!data.tbl_sn ? "저장" : "수정"}하시겠습니까?` + : `${!data.tbl_sn ? "저장" : "수정"}하시겠습니까?`; + if (!data.tbl_sn) { + await handleConfirm(async () => { + if (await confirm(confirmMsg)) { + insertLinkTableInfo({ + ...data, + mdfr_id: userUniqId, + tbl_sn: regTblInfo.count > 0 ? regTblInfo.tbl_sn : "", + }); + } + }); + } else { + await handleConfirm(async () => { + if (await confirm(confirmMsg)) { + updateLinkTableInfo({ + ...data, + tbl_sn: regTblInfo.count > 0 ? regTblInfo.tbl_sn : "", + mdfr_id: userUniqId, + }); + } + }); + } + }; + + const { tableComponent } = useDataTable({ + query, + tableOptions: { + columns, + state: { + pagination, + rowSelection, + }, + manualPagination: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + }, + dataTableOptions: { + onRowClick: handleRowClick, + viewPagination: true, + }, + }); + + return ( +
+
+
+ + setLinkSn(e.target.value)} + autoComplete="off" + /> + + setLinkNm(e.target.value)} + autoComplete="off" + /> + + +
+
+
+
+
+

연계 목록

+
+
+ {!query.isLoading ? ( +
{tableComponent}
+ ) : ( +
+
+ +
+
+ )} +
+
+
+
+

+ 연계 테이블 정보 +

+
+
+
+ + {selectedTableInfo && ( +
+ +
+ )} + + + + + + + + 연계 일련번호 + + {selectedTableInfo ? ( + ( + + + + + + )} + /> + ) : ( + - + )} + + + + 연계 명 + + {selectedTableInfo ? ( + ( + + + + + + )} + /> + ) : ( + - + )} + + + + 원천 테이블 명 + + {selectedTableInfo ? ( + ( + + + + + {tableForm.formState.errors.sou_tbl_nm && ( +

+ { + tableForm.formState.errors.sou_tbl_nm + .message + } +

+ )} +
+ )} + /> + ) : ( + - + )} +
+
+ + 원천 테이블 한글명 + + {selectedTableInfo ? ( + ( + + + + + {tableForm.formState.errors.sou_tbl_korn_nm && ( +

+ { + tableForm.formState.errors.sou_tbl_korn_nm + .message + } +

+ )} +
+ )} + /> + ) : ( + - + )} +
+
+ + 원천 테이블 설명 + + {selectedTableInfo ? ( + ( + + + + + {tableForm.formState.errors.sou_tbl_expln && ( +

+ { + tableForm.formState.errors.sou_tbl_expln + .message + } +

+ )} +
+ )} + /> + ) : ( + - + )} +
+
+ + * 변환 테이블 명 + + {selectedTableInfo ? ( + ( + + + + + {tableForm.formState.errors.trsf_tbl_nm && ( +

+ { + tableForm.formState.errors.trsf_tbl_nm + .message + } +

+ )} +
+ )} + /> + ) : ( + - + )} +
+
+ + * 변환 테이블 한글명 + + {selectedTableInfo ? ( + ( + + + + + {tableForm.formState.errors + .trsf_tbl_korn_nm && ( +

+ { + tableForm.formState.errors + .trsf_tbl_korn_nm.message + } +

+ )} +
+ )} + /> + ) : ( + - + )} +
+
+ + 변환 테이블 설명 + + {selectedTableInfo ? ( + ( + + + + + {tableForm.formState.errors.trsf_tbl_expln && ( +

+ { + tableForm.formState.errors.trsf_tbl_expln + .message + } +

+ )} +
+ )} + /> + ) : ( + - + )} +
+
+
+
+
+ + {selectedTableInfo ? ( + <> +
+ {tableColList.length > 0 && ( + + )} + {selectedTableInfo.tbl_sn && ( + + )} +
+ {selectedTableInfo.tbl_sn ? ( + tableColList.length > 0 ? ( +
+ +
+ ) : ( +
+ 등록된 테이블 컬럼이 없습니다. +
+ ) + ) : ( +
+ 테이블 정보를 등록해주세요. +
+ )} + + ) : ( +
+ 연계 데이터를 선택해주세요. +
+ )} +
+
+
+ +
+ ); +} + +export { LinkTablePage }; diff --git a/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx b/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx index d94d79c9a..baca00d74 100644 --- a/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx +++ b/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx @@ -77,7 +77,7 @@ function LoginlogPage() { }, }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setIsManualLoading(true); setPagination({ diff --git a/apps/cs/src/pages/menu/ui/menu-history-dialog.tsx b/apps/cs/src/pages/menu/ui/menu-history-dialog.tsx index a24b77308..78c34d440 100644 --- a/apps/cs/src/pages/menu/ui/menu-history-dialog.tsx +++ b/apps/cs/src/pages/menu/ui/menu-history-dialog.tsx @@ -91,7 +91,7 @@ function MenuHistoryDialog({ menuCd }: MenuHistoryDialogProps) { }, }); - const handleSearch = (e: React.FormEvent) => { + const handleSearch = (e: React.SubmitEvent) => { e.preventDefault(); e.stopPropagation(); query.refetch(); diff --git a/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx b/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx index eb942fe11..6956d6465 100644 --- a/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx +++ b/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx @@ -59,7 +59,7 @@ function OrganizationSearchDialog({ handleUserSelect(node.data as TreeUser); }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); e.stopPropagation(); setTree(filterUserOrganizationTreeDataByKeyword(treeData ?? [], keyword)); diff --git a/apps/cs/src/pages/module/ui/module-page.tsx b/apps/cs/src/pages/module/ui/module-page.tsx index 8593ba4b5..1749bdfbd 100644 --- a/apps/cs/src/pages/module/ui/module-page.tsx +++ b/apps/cs/src/pages/module/ui/module-page.tsx @@ -62,7 +62,7 @@ function ModulePage() { }, }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, diff --git a/apps/cs/src/pages/ntftalk-history/index.ts b/apps/cs/src/pages/ntftalk-history/index.ts new file mode 100644 index 000000000..e9871422b --- /dev/null +++ b/apps/cs/src/pages/ntftalk-history/index.ts @@ -0,0 +1 @@ +export { NtftalkHistoryPage } from "./ui/ntftalk-history-page"; diff --git a/apps/cs/src/pages/ntftalk-history/model/ntftalk-history-table-columns.tsx b/apps/cs/src/pages/ntftalk-history/model/ntftalk-history-table-columns.tsx new file mode 100644 index 000000000..637285075 --- /dev/null +++ b/apps/cs/src/pages/ntftalk-history/model/ntftalk-history-table-columns.tsx @@ -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[] { + 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 ( + + {v} + + ); + }, + }, + { + 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), + }, + ]; +} diff --git a/apps/cs/src/pages/ntftalk-history/ui/ntftalk-history-page.tsx b/apps/cs/src/pages/ntftalk-history/ui/ntftalk-history-page.tsx new file mode 100644 index 000000000..80434a15f --- /dev/null +++ b/apps/cs/src/pages/ntftalk-history/ui/ntftalk-history-page.tsx @@ -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, +): 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(""); + const [dateRange, setDateRange] = useState({ + from: null, + to: null, + }); + const [resultFilter, setResultFilter] = useState("ALL"); + const [systemFilter, setSystemFilter] = useState("ALL"); + + const [searchParams, setSearchParams] = + useState({ sort: "dateMtReport,desc" }); + const [pagination, setPagination] = useState({ + 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 ( +
+
+
+
+
+
+ + + +
+
+
+
+ + setContentInput(e.target.value)} + /> +
+
+ + setRecipientInput(e.target.value)} + placeholder="수신자 번호" + /> +
+
+ + +
+
+ + setSystemFilter(value)} + enabledAllSite + /> +
+
+
+
+ + +
+
+
+ +
+
+

알림톡 발송 이력

+
+
{tableComponent}
+
+
+ ); +} + +export { NtftalkHistoryPage }; diff --git a/apps/cs/src/pages/ntftalk-temp/index.ts b/apps/cs/src/pages/ntftalk-temp/index.ts new file mode 100644 index 000000000..a15c01db3 --- /dev/null +++ b/apps/cs/src/pages/ntftalk-temp/index.ts @@ -0,0 +1 @@ +export { NtftalkTempPage } from "./ui/ntftalk-temp-page"; diff --git a/apps/cs/src/pages/ntftalk-temp/model/ntftalk-temp-save-schema.ts b/apps/cs/src/pages/ntftalk-temp/model/ntftalk-temp-save-schema.ts new file mode 100644 index 000000000..911c4a277 --- /dev/null +++ b/apps/cs/src/pages/ntftalk-temp/model/ntftalk-temp-save-schema.ts @@ -0,0 +1,14 @@ +import { z } from "zod"; + +export const ntftalkTempSaveFormSchema = z.object({ + templateCode: z.string().min(1, "템플릿코드를 입력하세요."), + senderKey: z.string().min(1, "채널(senderKey)을 입력하세요."), + senderKeyType: z.string().min(1, "발신키 유형을 입력하세요."), + templateName: z.string().min(1, "템플릿명을 입력하세요."), + templateContent: z.string().min(1, "템플릿내용을 입력하세요."), + templateMessageType: z.string().min(1, "메시지유형을 입력하세요."), + templateEmphasizeType: z.string().min(1, "강조유형을 입력하세요."), + templateImageUrl: z.string().optional(), +}); + +export type NtftalkTempSaveFormData = z.infer; diff --git a/apps/cs/src/pages/ntftalk-temp/model/ntftalk-var-grid.ts b/apps/cs/src/pages/ntftalk-temp/model/ntftalk-var-grid.ts new file mode 100644 index 000000000..aa4892b1f --- /dev/null +++ b/apps/cs/src/pages/ntftalk-temp/model/ntftalk-var-grid.ts @@ -0,0 +1,19 @@ +import type { NtftalkVar } from "@/entities/ntftalk"; + +export interface NtftalkVarGridRow extends NtftalkVar { + clientId?: string; + isDraft?: boolean; +} + +export function createEmptyVarDraft(clientId: string): NtftalkVarGridRow { + return { + clientId, + isDraft: true, + varCd: "", + varNm: "", + varDesc: "", + sender: "", + regDt: "", + regr: "", + }; +} diff --git a/apps/cs/src/pages/ntftalk-temp/model/template-table-columns.tsx b/apps/cs/src/pages/ntftalk-temp/model/template-table-columns.tsx new file mode 100644 index 000000000..29df948ad --- /dev/null +++ b/apps/cs/src/pages/ntftalk-temp/model/template-table-columns.tsx @@ -0,0 +1,99 @@ +import type { ColumnDef, Row, Table } from "@tanstack/react-table"; +import { Checkbox } from "@common/components"; +import type { NtftalkTemp } from "@/entities/ntftalk"; + +function formatButtons(row: NtftalkTemp): string { + if (!row.buttons?.length) return "—"; + return row.buttons.map((b) => b.name).join(", "); +} + +export function createTemplateColumns(): ColumnDef[] { + return [ + { + id: "select", + header: ({ table }: { table: Table }) => ( +
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + role="presentation" + > + table.toggleAllPageRowsSelected(!!value)} + aria-label="전체 선택" + /> +
+ ), + cell: ({ row }: { row: Row }) => ( +
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + role="presentation" + > + row.toggleSelected(!!value)} + aria-label="행 선택" + /> +
+ ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: "senderKey", + header: "@채널", + }, + { + accessorKey: "templateCode", + header: "템플릿코드", + }, + { + accessorKey: "templateName", + header: "템플릿명", + }, + { + accessorKey: "templateMessageType", + header: "메시지유형", + }, + { + accessorKey: "templateContent", + header: "템플릿내용", + cell: ({ getValue }) => { + const v = getValue() as string; + if (!v) return "—"; + return ( + + {v} + + ); + }, + }, + { + accessorKey: "templateEmphasizeType", + header: "강조유형", + }, + { + id: "buttons", + header: "버튼정보", + cell: ({ row }) => formatButtons(row.original), + }, + { + accessorKey: "inspectionStatus", + header: "검수상태", + }, + { + accessorKey: "status", + header: "상태", + }, + { + accessorKey: "createdAt", + header: "등록일자", + }, + ]; +} diff --git a/apps/cs/src/pages/ntftalk-temp/model/var-table-columns.tsx b/apps/cs/src/pages/ntftalk-temp/model/var-table-columns.tsx new file mode 100644 index 000000000..3b0d39ced --- /dev/null +++ b/apps/cs/src/pages/ntftalk-temp/model/var-table-columns.tsx @@ -0,0 +1,130 @@ +import type { ColumnDef, Row, Table } from "@tanstack/react-table"; +import { Checkbox, Input } from "@common/components"; +import type { NtftalkVarGridRow } from "./ntftalk-var-grid"; + +export interface VarColumnHandlers { + onDraftFieldChange: ( + clientId: string, + field: "varCd" | "varNm" | "varDesc" | "sender", + value: string, + ) => void; +} + +export function createVarColumns( + handlers: VarColumnHandlers, +): ColumnDef[] { + return [ + { + id: "select", + header: ({ table }: { table: Table }) => ( +
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + role="presentation" + > + table.toggleAllPageRowsSelected(!!value)} + aria-label="전체 선택" + /> +
+ ), + cell: ({ row }: { row: Row }) => ( +
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + role="presentation" + > + row.toggleSelected(!!value)} + aria-label="행 선택" + /> +
+ ), + enableSorting: false, + }, + { + id: "seq", + header: "순번", + cell: ({ row, table }) => { + const pageIndex = table.getState().pagination.pageIndex; + const pageSize = table.getState().pagination.pageSize; + return pageIndex * pageSize + row.index + 1; + }, + }, + { + accessorKey: "varCd", + header: "변수코드", + cell: ({ row }) => { + const r = row.original; + if (r.isDraft && r.clientId) { + return ( + + handlers.onDraftFieldChange(r.clientId!, "varCd", e.target.value) + } + onClick={(e) => e.stopPropagation()} + /> + ); + } + return r.varCd; + }, + }, + { + accessorKey: "varNm", + header: "변수명", + cell: ({ row }) => { + const r = row.original; + if (r.isDraft && r.clientId) { + return ( + + handlers.onDraftFieldChange(r.clientId!, "varNm", e.target.value) + } + onClick={(e) => e.stopPropagation()} + /> + ); + } + return r.varNm; + }, + }, + { + accessorKey: "varDesc", + header: "설명", + cell: ({ row }) => { + const r = row.original; + if (r.isDraft && r.clientId) { + return ( + + handlers.onDraftFieldChange(r.clientId!, "varDesc", e.target.value) + } + onClick={(e) => e.stopPropagation()} + /> + ); + } + return r.varDesc ?? "—"; + }, + }, + { + accessorKey: "regDt", + header: "등록일자", + }, + { + accessorKey: "regr", + header: "등록자", + }, + ]; +} diff --git a/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-detail-dialog.tsx b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-detail-dialog.tsx new file mode 100644 index 000000000..aa08f92ca --- /dev/null +++ b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-detail-dialog.tsx @@ -0,0 +1,110 @@ +"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, + DialogHeader, + DialogTitle, + LoadingWithAnimation, + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from "@common/components"; + +interface NtftalkTempDetailDialogProps { + templateCode: string | null; + open: boolean; + onOpenChange: (open: boolean) => void; +} + +function NtftalkTempDetailDialog({ + templateCode, + open, + onOpenChange, +}: NtftalkTempDetailDialogProps) { + const query = useQuery({ + queryKey: [ENDPOINT.NTFTALK_SERVICE.NTFTALK_TEMP, "detail", templateCode], + queryFn: () => NtftalkTempApi.getNtftalkTemp(templateCode!), + enabled: open && !!templateCode, + }); + + const d = query.data; + + return ( + + + + 알림톡 템플릿 상세 + + {query.isLoading && ( +
+ +
+ )} + {query.isError && } + {d && ( + + + + 메시지 유형 + {d.templateMessageType} + + + @채널 + {d.senderKey} + + + 템플릿코드 + {d.templateCode} + + + 템플릿명 + {d.templateName} + + + 강조유형 + {d.templateEmphasizeType} + + + 템플릿내용 + {d.templateContent} + + + 검수상태 + {d.inspectionStatus} + + {/* + 보안템플릿 여부 + {d.templateEmphasizeType} + + + 비즈톡센터 사용여부 + {d.status} + */} + + 등록일 + {d.createdAt} + + + 템플릿 상태 + {d.status} + + +
+ )} +
+
+ ); +} + +export { NtftalkTempDetailDialog }; diff --git a/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-page.tsx b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-page.tsx new file mode 100644 index 000000000..787f33f93 --- /dev/null +++ b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-page.tsx @@ -0,0 +1,678 @@ +"use client"; + +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, + Input, + Label, + LoadingWithAnimation, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; + +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"; + +const ALL = "__ALL__"; + +const varDraftSaveSchema = z.object({ + varCd: z.string().min(1), + varNm: z.string().min(1), + varDesc: z.string().optional(), + sender: z.string().optional(), +}); + +interface VarDraftFormValues { + draftVars: NtftalkVarGridRow[]; +} + +function NtftalkTempPage() { + const queryClient = useQueryClient(); + const alert = useAlertStore((s) => s.setMessage); + const { confirm } = useConfirm(); + + const [templateCodeInput, setTemplateCodeInput] = useState(""); + const [templateNameInput, setTemplateNameInput] = useState(""); + const [templateContentInput, setTemplateContentInput] = useState(""); + const [senderKeyFilter, setSenderKeyFilter] = useState(ALL); + const [inspectionFilter, setInspectionFilter] = useState(ALL); + const [messageTypeFilter, setMessageTypeFilter] = useState(ALL); + + const [searchParams, setSearchParams] = useState({ + sort: "createdAt,desc", + }); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const [rowSelection, setRowSelection] = useState({}); + + const [varPagination, setVarPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const [varSearchParams] = useState({ + sort: "regDt,desc", + }); + const [varRowSelection, setVarRowSelection] = useState({}); + const varDraftForm = useForm({ + defaultValues: { draftVars: [] }, + }); + const { control, setValue } = varDraftForm; + const { append, remove, replace } = useFieldArray({ + control, + name: "draftVars", + }); + const draftVars = useWatch({ control, name: "draftVars" }) ?? []; + + const [detailCode, setDetailCode] = useState(null); + const [detailOpen, setDetailOpen] = useState(false); + const [registOpen, setRegistOpen] = useState(false); + + const tempQuery = useQuery({ + queryKey: [ENDPOINT.NTFTALK_SERVICE.NTFTALK_TEMP, searchParams, pagination], + queryFn: () => + NtftalkTempApi.getNtftalkTempPage({ + ...searchParams, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + placeholderData: keepPreviousData, + }); + + const varQuery = useQuery({ + queryKey: [ + ENDPOINT.NTFTALK_SERVICE.NTFTALK_VAR, + varSearchParams, + varPagination, + ], + queryFn: () => + NtftalkVarApi.getNtftalkVarPage({ + ...varSearchParams, + page: varPagination.pageIndex, + size: varPagination.pageSize, + }), + placeholderData: keepPreviousData, + }); + + const senderQuery = useQuery({ + queryKey: [ENDPOINT.NTFTALK_SERVICE.NTFTALK_BIZTALK, "sender"], + queryFn: () => NtftalkBiztalkApi.getSender(), + }); + + const senderKeyOptions = useMemo(() => { + if (senderQuery.data) return [senderQuery.data]; + return []; + }, [senderQuery.data]); + + const inspectionOptions = useMemo(() => { + const set = new Set(); + (tempQuery.data?.content ?? []).forEach((r) => { + if (r.inspectionStatus) set.add(r.inspectionStatus); + }); + return Array.from(set).sort(); + }, [tempQuery.data]); + + const messageTypeOptions = useMemo(() => { + const set = new Set(); + (tempQuery.data?.content ?? []).forEach((r) => { + if (r.templateMessageType) set.add(r.templateMessageType); + }); + return Array.from(set).sort(); + }, [tempQuery.data]); + + const templateColumns = useMemo(() => createTemplateColumns(), []); + + const { + tableComponent: templateTableComponent, + tableInstance: templateTable, + } = useDataTable({ + query: tempQuery, + tableOptions: { + columns: templateColumns, + state: { pagination, rowSelection }, + manualPagination: true, + enableRowSelection: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + }, + dataTableOptions: { + viewPagination: true, + onRowClick: (row: NtftalkTemp) => { + setDetailCode(row.templateCode); + setDetailOpen(true); + }, + }, + }); + + const draftIndexByClientId = useMemo(() => { + const indexByClientId = new Map(); + draftVars.forEach((row, index) => { + if (row.clientId) indexByClientId.set(row.clientId, index); + }); + return indexByClientId; + }, [draftVars]); + + const onDraftFieldChange = useCallback( + ( + clientId: string, + field: "varCd" | "varNm" | "varDesc" | "sender", + value: string, + ) => { + const draftIndex = draftIndexByClientId.get(clientId); + if (draftIndex === undefined) return; + setValue(`draftVars.${draftIndex}.${field}`, value, { + shouldDirty: true, + }); + }, + [draftIndexByClientId, setValue], + ); + + const varColumns = useMemo( + () => createVarColumns({ onDraftFieldChange }), + [onDraftFieldChange], + ); + + const varTableRows = useMemo((): NtftalkVarGridRow[] => { + const server = varQuery.data?.content ?? []; + if (varPagination.pageIndex === 0) return [...server, ...draftVars]; + return server; + }, [varQuery.data, draftVars, varPagination.pageIndex]); + + const varTable = useReactTable({ + data: varTableRows, + columns: varColumns, + state: { pagination: varPagination, rowSelection: varRowSelection }, + manualPagination: true, + rowCount: varQuery.data?.totalElements ?? 0, + pageCount: varQuery.data?.totalPages ?? 0, + enableRowSelection: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setVarPagination, + onRowSelectionChange: setVarRowSelection, + getRowId: (row) => + row.isDraft && row.clientId + ? `draft:${row.clientId}` + : `srv:${row.varCd}`, + }); + + const buildSearchParamsFromInputs = (): NtftalkTempSearchParams => ({ + sort: searchParams.sort ?? "createdAt,desc", + templateCode: templateCodeInput.trim() || undefined, + templateName: templateNameInput.trim() || undefined, + templateContent: templateContentInput.trim() || undefined, + senderKey: senderKeyFilter === ALL ? undefined : senderKeyFilter, + inspectionStatus: inspectionFilter === ALL ? undefined : inspectionFilter, + templateMessageType: + messageTypeFilter === ALL ? undefined : messageTypeFilter, + }); + + const handleSearch = (e: React.SubmitEvent) => { + e.preventDefault(); + setPagination((p) => ({ ...p, pageIndex: 0 })); + setSearchParams(buildSearchParamsFromInputs()); + }; + + const handleReset = () => { + setTemplateCodeInput(""); + setTemplateNameInput(""); + setTemplateContentInput(""); + setSenderKeyFilter(ALL); + setInspectionFilter(ALL); + setMessageTypeFilter(ALL); + setPagination((p) => ({ ...p, pageIndex: 0 })); + setSearchParams({ sort: "createdAt,desc" }); + }; + + const syncTemplatesMutation = useMutationWrapper({ + mutationFn: () => NtftalkBiztalkApi.syncTemplates({ senderKeyType: "S" }), + onSuccess: async (data) => { + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.NTFTALK_SERVICE.NTFTALK_TEMP], + }), + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.NTFTALK_SERVICE.NTFTALK_BIZTALK, "sender"], + }), + ]); + alert(data.message); + }, + }); + + const handleRefreshTemplates = () => { + syncTemplatesMutation.mutate(); + }; + + const deleteTemplatesMutation = useMutationWrapper({ + mutationFn: async (codes: string[]) => { + for (const code of codes) { + await NtftalkTempApi.deleteNtftalkTemp(code); + } + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: [ENDPOINT.NTFTALK_SERVICE.NTFTALK_TEMP], + }); + setRowSelection({}); + alert("삭제되었습니다."); + }, + }); + + const handleDeleteSelectedTemplates = async () => { + const rows = templateTable.getSelectedRowModel().rows; + if (rows.length === 0) { + alert("삭제할 템플릿을 선택해 주세요."); + return; + } + if ( + !(await confirm( + `선택한 ${rows.length}건의 템플릿을 삭제(비표시) 처리하시겠습니까?`, + )) + ) + return; + deleteTemplatesMutation.mutate(rows.map((r) => r.original.templateCode)); + }; + + const inspectionRequestMutation = useMutationWrapper({ + mutationFn: (requestList: NtftalkBiztalkInspectionRequestItem[]) => + NtftalkBiztalkApi.requestInspection(requestList), + onSuccess: async (data) => { + await queryClient.invalidateQueries({ + queryKey: [ENDPOINT.NTFTALK_SERVICE.NTFTALK_TEMP], + }); + setRowSelection({}); + alert(data.message); + }, + }); + + const handleInspectionRequest = async () => { + const rows = templateTable.getSelectedRowModel().rows; + if (rows.length === 0) { + alert("검수요청할 템플릿을 선택해 주세요."); + return; + } + + const requestList: NtftalkBiztalkInspectionRequestItem[] = []; + for (const row of rows) { + const { senderKey, templateCode, senderKeyType } = row.original; + if (!senderKey || !templateCode) { + alert("선택 항목에 발신키 또는 템플릿코드가 없습니다."); + return; + } + requestList.push({ + senderKey, + templateCode, + senderKeyType: senderKeyType || "S", + }); + } + + if ( + !(await confirm(`선택한 ${requestList.length}건을 검수요청하시겠습니까?`)) + ) + return; + inspectionRequestMutation.mutate(requestList); + }; + + const handleSaveTemplateGrid = () => { + alert("템플릿 그리드 셀 편집·일괄 저장은 별도 구현 시 연동합니다."); + }; + + const handleAddVarRow = () => { + append(createEmptyVarDraft(crypto.randomUUID())); + setVarPagination((p) => ({ ...p, pageIndex: 0 })); + }; + + const deleteVarsMutation = useMutationWrapper({ + mutationFn: async (codes: string[]) => { + for (const code of codes) { + await NtftalkVarApi.deleteNtftalkVar(code); + } + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: [ENDPOINT.NTFTALK_SERVICE.NTFTALK_VAR], + }); + setVarRowSelection({}); + alert("삭제되었습니다."); + }, + }); + + const handleDeleteSelectedVars = async () => { + const selected = varTable.getSelectedRowModel().rows; + if (selected.length === 0) { + alert("삭제할 변수를 선택해 주세요."); + return; + } + if (!(await confirm(`선택한 ${selected.length}건을 삭제하시겠습니까?`))) + return; + + const serverCodes: string[] = []; + const draftIndexes: number[] = []; + selected.forEach((r) => { + const o = r.original; + if (o.isDraft && o.clientId) { + const draftIndex = draftIndexByClientId.get(o.clientId); + if (draftIndex !== undefined) draftIndexes.push(draftIndex); + } else if (o.varCd) serverCodes.push(o.varCd); + }); + + if (draftIndexes.length) + remove([...new Set(draftIndexes)].sort((a, b) => b - a)); + + if (serverCodes.length) deleteVarsMutation.mutate(serverCodes); + }; + + const saveVarsMutation = useMutationWrapper({ + mutationFn: async (drafts: NtftalkVarGridRow[]) => { + const regr = getCookie(AUTH_USER_ID) ?? undefined; + for (const d of drafts) { + const parsed = varDraftSaveSchema.safeParse({ + varCd: d.varCd, + varNm: d.varNm, + varDesc: d.varDesc || undefined, + sender: d.sender || undefined, + }); + if (!parsed.success) + throw new Error( + parsed.error.issues[0]?.message ?? "유효하지 않은 입력입니다.", + ); + await NtftalkVarApi.saveNtftalkVar({ + varCd: parsed.data.varCd, + varNm: parsed.data.varNm, + varDesc: parsed.data.varDesc, + sender: parsed.data.sender, + regr, + }); + } + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: [ENDPOINT.NTFTALK_SERVICE.NTFTALK_VAR], + }); + replace([]); + setVarRowSelection({}); + alert("저장되었습니다."); + }, + }); + + const handleSaveVars = () => { + if (draftVars.length === 0) { + alert("저장할 신규 변수 행이 없습니다. 변수추가로 행을 추가하세요."); + return; + } + saveVarsMutation.mutate(draftVars); + }; + + const varTableComponent = (() => { + if (varQuery.isLoading) + return ( +
+ +
+ ); + if (varQuery.isError) return ; + return ; + })(); + + return ( +
+
+
+
+
+
+ + setTemplateCodeInput(e.target.value)} + /> +
+
+ + setTemplateNameInput(e.target.value)} + /> +
+
+ + setTemplateContentInput(e.target.value)} + /> +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+

+ 알림톡 템플릿 관리 +

+
+ + + + + +
+
+
{templateTableComponent}
+
+ +
+
+

변수 관리

+
+ + + +
+
+
{varTableComponent}
+
+ + { + setDetailOpen(o); + if (!o) setDetailCode(null); + }} + /> + +
+ ); +} + +export { NtftalkTempPage }; diff --git a/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-regist-dialog.tsx b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-regist-dialog.tsx new file mode 100644 index 000000000..8a537e7a2 --- /dev/null +++ b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-regist-dialog.tsx @@ -0,0 +1,442 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { useForm } from "react-hook-form"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { + keepPreviousData, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; +import { + getCoreRowModel, + 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, + Label, + LoadingWithAnimation, + RadioGroup, + RadioGroupItem, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Textarea, +} from "@common/components"; + +import { + type NtftalkTempSaveFormData, + ntftalkTempSaveFormSchema, +} from "../model/ntftalk-temp-save-schema"; +import { createVarColumns } from "../model/var-table-columns"; + +interface NtftalkTempRegistDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +const TEMPLATE_MESSAGE_TYPE_BASIC = "BA"; +const SECURITY_TEMPLATE_EMPHASIZE_TYPE = { + enabled: "SECURITY", + disabled: "NONE", +} as const; + +function NtftalkTempRegistDialog({ + open, + onOpenChange, +}: NtftalkTempRegistDialogProps) { + const [isVarDialogOpen, setIsVarDialogOpen] = useState(false); + const [varPagination, setVarPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const [varRowSelection, setVarRowSelection] = useState({}); + const [varSearchParams] = useState({ + sort: "regDt,desc", + }); + + const queryClient = useQueryClient(); + const alert = useAlertStore((s) => s.setMessage); + const senderQuery = useQuery({ + queryKey: [ENDPOINT.NTFTALK_SERVICE.NTFTALK_BIZTALK, "sender"], + queryFn: () => NtftalkBiztalkApi.getSender(), + }); + const varQuery = useQuery({ + queryKey: [ + ENDPOINT.NTFTALK_SERVICE.NTFTALK_VAR, + varSearchParams, + varPagination, + ], + queryFn: () => + NtftalkVarApi.getNtftalkVarPage({ + ...varSearchParams, + page: varPagination.pageIndex, + size: varPagination.pageSize, + }), + placeholderData: keepPreviousData, + enabled: isVarDialogOpen, + }); + + const varColumns = useMemo( + () => + createVarColumns({ + onDraftFieldChange: () => {}, + }), + [], + ); + const { tableComponent, tableInstance } = useDataTable({ + query: varQuery, + tableOptions: { + columns: varColumns, + state: { pagination: varPagination, rowSelection: varRowSelection }, + manualPagination: true, + enableRowSelection: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setVarPagination, + onRowSelectionChange: setVarRowSelection, + getRowId: (row) => `srv:${row.varCd}`, + }, + dataTableOptions: { + viewPagination: true, + }, + }); + + const form = useForm({ + resolver: zodResolver(ntftalkTempSaveFormSchema), + defaultValues: { + templateCode: "", + senderKey: "", + senderKeyType: "S", + templateName: "", + templateContent: "", + templateMessageType: TEMPLATE_MESSAGE_TYPE_BASIC, + templateEmphasizeType: SECURITY_TEMPLATE_EMPHASIZE_TYPE.enabled, + templateImageUrl: "", + }, + }); + + useEffect(() => { + if (!open) return; + form.reset(); + }, [open]); + + const { mutateAsync: saveTemp, isPending } = useMutationWrapper({ + mutationFn: (data: NtftalkTempSaveFormData) => + NtftalkTempApi.saveNtftalkTemp({ + ...data, + templateImageUrl: data.templateImageUrl || undefined, + }), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: [ENDPOINT.NTFTALK_SERVICE.NTFTALK_TEMP], + }); + alert("등록되었습니다."); + form.reset(); + onOpenChange(false); + }, + }); + + const onSubmit = form.handleSubmit((data) => + saveTemp({ + ...data, + senderKeyType: "S", + templateMessageType: TEMPLATE_MESSAGE_TYPE_BASIC, + }), + ); + + useEffect(() => { + if (!senderQuery.data) return; + if (form.getValues("senderKey")) return; + form.setValue("senderKey", senderQuery.data.senderKey); + }, [senderQuery.data, form]); + + function handleInsertVariable() { + setIsVarDialogOpen(true); + } + + function handleApplySelectedVariables() { + const rows = tableInstance.getSelectedRowModel().rows; + if (rows.length === 0) { + alert("추가할 변수를 선택해 주세요."); + return; + } + + const varCodes = rows.map((row) => row.original.varCd?.trim() ?? ""); + if (varCodes.some((varCd) => !varCd)) { + alert("선택 항목 중 변수코드가 없는 항목이 있습니다."); + return; + } + + const currentValue = form.getValues("templateContent"); + const tokens = varCodes.map((varCd) => `${varCd}`); + const appendValue = tokens.join("\n"); + const nextValue = currentValue + ? `${currentValue}\n${appendValue}` + : appendValue; + + form.setValue("templateContent", nextValue, { + shouldValidate: true, + shouldDirty: true, + }); + setVarRowSelection({}); + setIsVarDialogOpen(false); + } + + const varTableComponent = (() => { + if (varQuery.isLoading) + return ( +
+ +
+ ); + if (varQuery.isError) return ; + return tableComponent; + })(); + + return ( + + + + 알림 템플릿 등록 + +
+ + + + + + + + + + 메시지 유형 + + 기본형 + + + + 채널 + + + ( + + + + + + + )} + /> + + + + + 템플릿코드 + + + ( + + + + + + + )} + /> + + + + + 템플릿명 + + + ( + + + + + + + )} + /> + + + + + 보안템플릿여부 + + + ( + + + +
+ + +
+
+ + +
+
+
+ +
+ )} + /> +
+
+ + + 템플릿내용 + + + ( + +
+ +
+ +