diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 000000000..6d405c2f8 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,33 @@ +name: Deploy Frontend + +on: + push: + branches: + - deploy + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -p 28 -H 192.168.0.4 >> ~/.ssh/known_hosts + + - name: Deploy + run: | + ssh -i ~/.ssh/deploy_key -p 28 onasys@192.168.0.4 << 'EOF' + cd ~/frontend + git restore . + git clean -fd + git pull origin deploy + pnpm install + pnpm turbo build + pm2 reload all + EOF 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/package.json b/apps/cm/package.json index bde698393..2472516e4 100644 --- a/apps/cm/package.json +++ b/apps/cm/package.json @@ -12,6 +12,9 @@ "@common/assets": "workspace:*", "@common/components": "workspace:*", "@common/config": "workspace:*", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@fontsource-variable/noto-sans-kr": "^5.2.5", "@frontend-opensource/use-react-hooks": "^1.1.4", "@fullcalendar/core": "^6.1.17", 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} +
)}
logo @@ -218,25 +228,36 @@ function LoginPage() {
- {error && ( -
+
{error}
)} {successMessage && ( -
+
{successMessage}
)} -
- 계정이 없으신가요? +
+ + 계정이 없으신가요?{" "} + +
-
-
- - -
+ ) : ( + !error &&
사용자 정보를 불러오는 중...
+ )} + + + +
- ) : ( - !error &&
사용자 정보를 불러오는 중...
- )} - - - - - - - - {/* 두번째 라인 */} -
-
- -
-
- -
-
- -
- - {/* 배너 영역 */} -
-
+ + {/* 두번째 라인 - 드래그 가능한 4개 패널 */} + {isMounted ? ( + + p.id)} + strategy={rectSortingStrategy} + > +
+ {bottomPanels.map((panel) => ( + +
{panel.component}
+
+ ))} +
+
+
+ ) : ( +
+ {bottomPanels.map((panel) => ( +
+
{panel.component}
+
+ ))} +
+ )} ); diff --git a/apps/cm/src/pages/dashboard/ui/datastatus-list.tsx b/apps/cm/src/pages/dashboard/ui/datastatus-list.tsx index bae89e338..74e10afea 100644 --- a/apps/cm/src/pages/dashboard/ui/datastatus-list.tsx +++ b/apps/cm/src/pages/dashboard/ui/datastatus-list.tsx @@ -5,6 +5,7 @@ import { useQuery } from "@tanstack/react-query"; import { ENDPOINT } from "@/shared/config"; import { getDataStatusList, type DataStatus } from "../api/datastatus-service"; import type { PageableList } from "@/shared/model"; +import { useDragHandle } from "./sortable-panel"; import { Carousel, @@ -35,6 +36,7 @@ const getRegionInfo = (regionName: string) => { }; const DataStatusList: React.FC = () => { + const { dragHandleProps, isDragging } = useDragHandle(); const plugin = useRef( Autoplay({ delay: 3000, stopOnInteraction: true }) ); @@ -106,7 +108,11 @@ const DataStatusList: React.FC = () => { return ( <> -
+

자료현황

diff --git a/apps/cm/src/pages/dashboard/ui/fo-fclt-cnt-rt.tsx b/apps/cm/src/pages/dashboard/ui/fo-fclt-cnt-rt.tsx index 2a0264e43..5db22db6e 100644 --- a/apps/cm/src/pages/dashboard/ui/fo-fclt-cnt-rt.tsx +++ b/apps/cm/src/pages/dashboard/ui/fo-fclt-cnt-rt.tsx @@ -9,8 +9,10 @@ import { Doughnut } from "react-chartjs-2"; import type { Chart as ChartJS } from "chart.js"; // import type { mapOperFcltItem, mapBasinData } from "../model/fo-interface"; import { OperationApi, operFcltStore, basinStore } from "../api/fo-service"; +import { useDragHandle } from "./sortable-panel"; const FoFcltCntRt: React.FC = () => { + const { dragHandleProps, isDragging } = useDragHandle(); const [year, setYear] = useState(null); const { selectedBasin, setSelectedBasin } = useStore(basinStore); const operFcltStatus = useStore(operFcltStore, (state) => state.operFcltStatus); @@ -57,9 +59,18 @@ const FoFcltCntRt: React.FC = () => { return (
-
+

수계별 시설운영 개소수 및 가동률

- +
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > + +
diff --git a/apps/cm/src/pages/dashboard/ui/fo-wqlt-Infl-effl.tsx b/apps/cm/src/pages/dashboard/ui/fo-wqlt-Infl-effl.tsx index 4c01fbdd0..6b416c5ed 100644 --- a/apps/cm/src/pages/dashboard/ui/fo-wqlt-Infl-effl.tsx +++ b/apps/cm/src/pages/dashboard/ui/fo-wqlt-Infl-effl.tsx @@ -14,6 +14,7 @@ import { import type { OperWqltContent } from "../model/fo-interface"; import { OperationApi, basinStore } from "../api/fo-service"; import { defaultInitOperWqltState } from "../api/fo-service"; +import { useDragHandle } from "./sortable-panel"; // Chart.js 등록 ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend); @@ -21,6 +22,7 @@ ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, T type WaterQualityType = 'BOD' | 'TOC' | 'SS' | 'TP' | 'TN'; const FoWqltInflEffl: React.FC = () => { + const { dragHandleProps, isDragging } = useDragHandle(); const { selectedBasin, setSelectedBasin } = useStore(basinStore); const [selectedTab, setSelectedTab] = useState('BOD'); const [wqltData, setWqltData] = useState([]); @@ -195,9 +197,18 @@ const FoWqltInflEffl: React.FC = () => { return (
-
+

수계별 유입ㆍ방류수질 분석

- +
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > + +
diff --git a/apps/cm/src/pages/dashboard/ui/notice-list.tsx b/apps/cm/src/pages/dashboard/ui/notice-list.tsx index 502973225..73b2788df 100644 --- a/apps/cm/src/pages/dashboard/ui/notice-list.tsx +++ b/apps/cm/src/pages/dashboard/ui/notice-list.tsx @@ -10,9 +10,11 @@ import { noticePostTableColumns, } from "../model/table-columns"; import { Button } from "@common/components"; +import { useDragHandle } from "./sortable-panel"; const NoticeList: React.FC = () => { + const { dragHandleProps, isDragging } = useDragHandle(); const router = useRouter(); const noticePostQuery = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS], @@ -51,9 +53,18 @@ const NoticeList: React.FC = () => { return (
-
+

공지사항

- +
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > + +
{noticePostTableComponent}
diff --git a/apps/cm/src/pages/dashboard/ui/reuse-chart.tsx b/apps/cm/src/pages/dashboard/ui/reuse-chart.tsx index edcc19b97..55d9e6abb 100644 --- a/apps/cm/src/pages/dashboard/ui/reuse-chart.tsx +++ b/apps/cm/src/pages/dashboard/ui/reuse-chart.tsx @@ -8,6 +8,7 @@ import { Line } from 'react-chartjs-2'; import { Button } from "@common/components"; import type { ReuseStatusContent } from "../model/fo-interface"; import { OperationApi, reuseStore } from "../api/fo-service"; +import { useDragHandle } from "./sortable-panel"; import { Chart as ChartJS, CategoryScale, @@ -36,6 +37,7 @@ ChartJS.register( const ReuseChart: React.FC = () => { + const { dragHandleProps, isDragging } = useDragHandle(); const [activeIndex, setActiveIndex] = useState(0); const intervalRef = useRef(null); const reuseStatusData = useStore( @@ -183,9 +185,18 @@ const ReuseChart: React.FC = () => { return (
-
+

{chartItems[activeIndex]?.title}

- +
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > + +
diff --git a/apps/cm/src/pages/dashboard/ui/reuse-status.tsx b/apps/cm/src/pages/dashboard/ui/reuse-status.tsx index d1e7c4c15..6c596eba9 100644 --- a/apps/cm/src/pages/dashboard/ui/reuse-status.tsx +++ b/apps/cm/src/pages/dashboard/ui/reuse-status.tsx @@ -7,8 +7,10 @@ import { useStore } from "zustand"; import { Button } from "@common/components"; import type { ReuseStatusContent } from "../model/fo-interface"; import { OperationApi, reuseStore} from "../api/fo-service"; +import { useDragHandle } from "./sortable-panel"; const ReuseStatus: React.FC = () => { + const { dragHandleProps, isDragging } = useDragHandle(); // const [reuseStatusData, setReuseStatusData] = useState([]); const reuseStatusData = useStore( reuseStore, @@ -32,9 +34,18 @@ const ReuseStatus: React.FC = () => { return (
-
+

물재이용 현황 (재이용량, m³)

- +
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > + +
diff --git a/apps/cm/src/pages/dashboard/ui/sn-old-ppl.tsx b/apps/cm/src/pages/dashboard/ui/sn-old-ppl.tsx index b1a34139c..fa502e7c5 100644 --- a/apps/cm/src/pages/dashboard/ui/sn-old-ppl.tsx +++ b/apps/cm/src/pages/dashboard/ui/sn-old-ppl.tsx @@ -17,12 +17,14 @@ import { Bar } from "react-chartjs-2"; import { Button } from "@common/components"; import type { PplLengthContent, ChartDataSet, OldPplChartData } from "../model/sn-interface"; import { SewerageApi, useOldPplStore,useMapOldPplStore, transformChartDataToMapData } from "../api/sn-service"; +import { useDragHandle } from "./sortable-panel"; // ChartJS 컴포넌트 등록 ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); export default function SnOldPpl() { + const { dragHandleProps, isDragging } = useDragHandle(); const [oldPplLenData, setOldPplLenData] = useState([]); const {chartData, setChartData} = useOldPplStore(); const {setMapData} = useMapOldPplStore(); @@ -170,9 +172,18 @@ export default function SnOldPpl() { return (
-
+

노후 하수관로 현황

- +
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > + +
diff --git a/apps/cm/src/pages/dashboard/ui/sn-ppl-status.tsx b/apps/cm/src/pages/dashboard/ui/sn-ppl-status.tsx index 014f592c4..022430da0 100644 --- a/apps/cm/src/pages/dashboard/ui/sn-ppl-status.tsx +++ b/apps/cm/src/pages/dashboard/ui/sn-ppl-status.tsx @@ -17,6 +17,7 @@ import { Bar } from "react-chartjs-2"; import { Button } from "@common/components"; import type { PplLengthContent } from "../model/sn-interface"; import { SewerageApi,useUsePplStore } from "../api/sn-service"; +import { useDragHandle } from "./sortable-panel"; interface ChartDataSet { label: string; @@ -29,6 +30,7 @@ interface ChartDataSet { ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); export default function SnPplStatus() { + const { dragHandleProps, isDragging } = useDragHandle(); const {chartData, setChartData} = useUsePplStore(); const [usgPplLenData, setUsgPplLenData] = useState([]); @@ -157,10 +159,18 @@ export default function SnPplStatus() { }; return (
-
+

오수관로 및 우수관로 현황

- - +
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > + +
diff --git a/apps/cm/src/pages/dashboard/ui/sortable-panel.tsx b/apps/cm/src/pages/dashboard/ui/sortable-panel.tsx new file mode 100644 index 000000000..1fb891980 --- /dev/null +++ b/apps/cm/src/pages/dashboard/ui/sortable-panel.tsx @@ -0,0 +1,64 @@ +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { createContext, useContext } from "react"; + +interface SortablePanelProps { + id: string; + children: React.ReactNode; + className?: string; +} + +interface DragHandleContextValue { + dragHandleProps: React.HTMLAttributes; + isDragging: boolean; +} + +const DragHandleContext = createContext(null); + +export function SortablePanel({ id, children, className }: SortablePanelProps) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + + const contextValue: DragHandleContextValue = { + dragHandleProps: { ...attributes, ...listeners }, + isDragging, + }; + + return ( + +
+ {children} +
+
+ ); +} + +export function useDragHandle() { + const context = useContext(DragHandleContext); + + // Context가 없을 때 (서버 렌더링 또는 isMounted = false) 기본값 반환 + if (!context) { + return { + dragHandleProps: {}, + isDragging: false, + }; + } + + return context; +} diff --git a/apps/cm/src/pages/dashboard/ui/support-list.tsx b/apps/cm/src/pages/dashboard/ui/support-list.tsx index f62cce9b1..f37dad7eb 100644 --- a/apps/cm/src/pages/dashboard/ui/support-list.tsx +++ b/apps/cm/src/pages/dashboard/ui/support-list.tsx @@ -7,8 +7,10 @@ import { useDataTable } from "@/shared/lib"; import type { PageableList } from "@/shared/model"; import { supportTableColumns } from "../model/support-columns"; import { getSupportList, type Support } from "../api/support-service"; +import { useDragHandle } from "./sortable-panel"; const SupportList: React.FC = () => { + const { dragHandleProps, isDragging } = useDragHandle(); const supportQuery = useQuery>({ queryKey: [ENDPOINT.IC_SERVICE.GROUPING_STATISTICS], queryFn: async () => { @@ -29,7 +31,11 @@ const SupportList: React.FC = () => { return (
-
+

지원현황

{supportTableComponent} diff --git a/apps/cm/src/pages/users/ui/users-form.tsx b/apps/cm/src/pages/users/ui/users-form.tsx index 7920ce758..a8a5a86f3 100644 --- a/apps/cm/src/pages/users/ui/users-form.tsx +++ b/apps/cm/src/pages/users/ui/users-form.tsx @@ -102,35 +102,35 @@ function UsersForm({ return ( authSeCd?.filter( (code) => - code.lwpoCd === "ROLE_USER1" || - code.lwpoCd === "ROLE_USER2" || - code.lwpoCd === "ROLE_USER3", + code.lwpoCd === "ROLE_USER01" || + code.lwpoCd === "ROLE_USER02" || + code.lwpoCd === "ROLE_USER03", ) ?? [] ); if (watchOgdpSeCd === "3" || watchOgdpSeCd === "4") return ( authSeCd?.filter( (code) => - code.lwpoCd === "ROLE_USER4" || code.lwpoCd === "ROLE_USER5", + code.lwpoCd === "ROLE_USER04" || code.lwpoCd === "ROLE_USER05", ) ?? [] ); return []; }, [authSeCd, watchOgdpSeCd]); const laeSeCdSelectData = useMemo(() => { - if (watchDtlInstCd === "ROLE_USER1") + if (watchDtlInstCd === "ROLE_USER01") return laeSeCd?.filter((code) => code.lwpoCd === "G102") ?? []; - if (watchDtlInstCd === "ROLE_USER2") + if (watchDtlInstCd === "ROLE_USER02") return laeSeCd?.filter((code) => code.lwpoCd === "G101") ?? []; - if (watchDtlInstCd === "ROLE_USER3") + if (watchDtlInstCd === "ROLE_USER03") return laeSeCd?.filter((code) => code.lwpoCd.startsWith("G2")) ?? []; - if (watchDtlInstCd === "ROLE_USER4") + if (watchDtlInstCd === "ROLE_USER04") return ( laeSeCd?.filter( (code) => code.lwpoCd.startsWith("G3") && code.lwpoCd.length === 4, ) ?? [] ); - if (watchDtlInstCd === "ROLE_USER5") + if (watchDtlInstCd === "ROLE_USER05") return ( laeSeCd?.filter( (code) => code.lwpoCd.startsWith("G3") && code.lwpoCd.length === 7, @@ -622,8 +622,9 @@ function UsersForm({ onValueChange={field.onChange} disabled={ !( - watchDtlInstCd === "ROLE_USER4" || - watchDtlInstCd === "ROLE_USER5" + watchDtlInstCd === "ROLE_USER03" || + watchDtlInstCd === "ROLE_USER04" || + watchDtlInstCd === "ROLE_USER05" ) } > diff --git a/apps/cm/src/pages/users/ui/users-page.tsx b/apps/cm/src/pages/users/ui/users-page.tsx index 5362b8b42..5670eb9d3 100644 --- a/apps/cm/src/pages/users/ui/users-page.tsx +++ b/apps/cm/src/pages/users/ui/users-page.tsx @@ -102,7 +102,7 @@ function UsersPage() { }, }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, diff --git a/apps/common/features/src/pages/(post)/faq/ui/faq-page.tsx b/apps/common/features/src/pages/(post)/faq/ui/faq-page.tsx index 6255bb27a..6fb5dc96c 100644 --- a/apps/common/features/src/pages/(post)/faq/ui/faq-page.tsx +++ b/apps/common/features/src/pages/(post)/faq/ui/faq-page.tsx @@ -83,7 +83,7 @@ function FaqPage({ bbsId }: { bbsId: string }) { } }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); refetch(); }; diff --git a/apps/common/features/src/pages/auth/login/ui/login-page.tsx b/apps/common/features/src/pages/auth/login/ui/login-page.tsx index 4700a691b..95945c8ed 100644 --- a/apps/common/features/src/pages/auth/login/ui/login-page.tsx +++ b/apps/common/features/src/pages/auth/login/ui/login-page.tsx @@ -6,7 +6,7 @@ import { LoginApi } from "@/entities/auth"; import "./login.css"; function LoginPage() { - const handleSubmit = async (e: React.FormEvent) => { + const handleSubmit = async (e: React.SubmitEvent) => { e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); diff --git a/apps/common/features/src/pages/search/ui/search-page.tsx b/apps/common/features/src/pages/search/ui/search-page.tsx index dd1aec0b2..8324803d7 100644 --- a/apps/common/features/src/pages/search/ui/search-page.tsx +++ b/apps/common/features/src/pages/search/ui/search-page.tsx @@ -33,7 +33,7 @@ function SearchPage() { queryFn: () => SearchApi.searchPost({ keyword: keyword, siteId }), }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); setKeyword(formData.get("keyword") as string); @@ -84,7 +84,8 @@ function SearchPage() { 전체 ( - { (data?.contsList?.length ?? 0) + (data?.boardList?.totalElements ?? 0)} + {(data?.contsList?.length ?? 0) + + (data?.boardList?.totalElements ?? 0)} 건) diff --git a/apps/common/features/src/widgets/post/ui/post-page.tsx b/apps/common/features/src/widgets/post/ui/post-page.tsx index 7aaa390fc..922a0e910 100644 --- a/apps/common/features/src/widgets/post/ui/post-page.tsx +++ b/apps/common/features/src/widgets/post/ui/post-page.tsx @@ -134,7 +134,7 @@ function PostPage({ bbsId }: { bbsId: string }) { } }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, diff --git a/apps/cs/.env.example b/apps/cs/.env.example index 9f786831b..19231af8a 100644 --- a/apps/cs/.env.example +++ b/apps/cs/.env.example @@ -1,7 +1,8 @@ # apps/cs/src/shared/config/env.ts 참고 NEXT_PUBLIC_PORT=3000 NEXT_PUBLIC_PROXY_HOST=http://localhost:3000 +SERVER_API_URL=http://192.168.0.102:8000 NEXT_PUBLIC_SITE_ID=CS NEXT_PUBLIC_SUB_DOMAIN=http://localhost:3000 -NEXT_PUBLIC_SERVER_API_URL=http://192.168.0.102:8000 -NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2 \ No newline at end of file +NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2 +NEXT_PUBLIC_TINY_BASE_URL= \ No newline at end of file diff --git a/apps/cs/app/auth/login/page.tsx b/apps/cs/app/(auth)/auth/login/page.tsx similarity index 100% rename from apps/cs/app/auth/login/page.tsx rename to apps/cs/app/(auth)/auth/login/page.tsx diff --git a/apps/cs/app/(auth)/layout.tsx b/apps/cs/app/(auth)/layout.tsx new file mode 100644 index 000000000..33651c590 --- /dev/null +++ b/apps/cs/app/(auth)/layout.tsx @@ -0,0 +1,17 @@ +import "@/app/styles/base.css"; + +export const metadata = { + title: "유역하수도 통합 정보플랫폼", +}; + +export default function AuthLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/apps/cs/app/(with-base-layout)/admin/page.tsx b/apps/cs/app/(main)/(with-base-layout)/admin/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/admin/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/admin/page.tsx diff --git a/apps/cs/app/(with-base-layout)/apilog/page.tsx b/apps/cs/app/(main)/(with-base-layout)/apilog/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/apilog/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/apilog/page.tsx diff --git a/apps/cs/app/(with-base-layout)/banner/page.tsx b/apps/cs/app/(main)/(with-base-layout)/banner/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/banner/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/banner/page.tsx diff --git a/apps/cs/app/(with-base-layout)/banner/register/[sequential]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/banner/register/[sequential]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/banner/register/[sequential]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/banner/register/[sequential]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/banner/register/page.tsx b/apps/cs/app/(main)/(with-base-layout)/banner/register/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/banner/register/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/banner/register/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsfaq/[bbsId]/[pstNo]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsfaq/[bbsId]/[pstNo]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/(post)/bbsfaq/[bbsId]/[pstNo]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsfaq/[bbsId]/[pstNo]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsfaq/[bbsId]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsfaq/[bbsId]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/(post)/bbsfaq/[bbsId]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsfaq/[bbsId]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsfaq/[bbsId]/register/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsfaq/[bbsId]/register/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/(post)/bbsfaq/[bbsId]/register/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsfaq/[bbsId]/register/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsimage/[bbsId]/[pstNo]/modify/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsimage/[bbsId]/[pstNo]/modify/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/(post)/bbsimage/[bbsId]/[pstNo]/modify/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsimage/[bbsId]/[pstNo]/modify/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsimage/[bbsId]/[pstNo]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsimage/[bbsId]/[pstNo]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/(post)/bbsimage/[bbsId]/[pstNo]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsimage/[bbsId]/[pstNo]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsimage/[bbsId]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsimage/[bbsId]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/(post)/bbsimage/[bbsId]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsimage/[bbsId]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsimage/[bbsId]/register/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsimage/[bbsId]/register/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/(post)/bbsimage/[bbsId]/register/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsimage/[bbsId]/register/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsmovie/[bbsId]/[pstNo]/modify/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsmovie/[bbsId]/[pstNo]/modify/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/(post)/bbsmovie/[bbsId]/[pstNo]/modify/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsmovie/[bbsId]/[pstNo]/modify/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsmovie/[bbsId]/[pstNo]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsmovie/[bbsId]/[pstNo]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/(post)/bbsmovie/[bbsId]/[pstNo]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsmovie/[bbsId]/[pstNo]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsmovie/[bbsId]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsmovie/[bbsId]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/(post)/bbsmovie/[bbsId]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsmovie/[bbsId]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsmovie/[bbsId]/register/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsmovie/[bbsId]/register/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/(post)/bbsmovie/[bbsId]/register/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsmovie/[bbsId]/register/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsnormal/[bbsId]/[pstNo]/answer/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsnormal/[bbsId]/[pstNo]/answer/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/(post)/bbsnormal/[bbsId]/[pstNo]/answer/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsnormal/[bbsId]/[pstNo]/answer/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsnormal/[bbsId]/[pstNo]/modify/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsnormal/[bbsId]/[pstNo]/modify/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/(post)/bbsnormal/[bbsId]/[pstNo]/modify/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsnormal/[bbsId]/[pstNo]/modify/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsnormal/[bbsId]/[pstNo]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsnormal/[bbsId]/[pstNo]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/(post)/bbsnormal/[bbsId]/[pstNo]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsnormal/[bbsId]/[pstNo]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsnormal/[bbsId]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsnormal/[bbsId]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/(post)/bbsnormal/[bbsId]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsnormal/[bbsId]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsnormal/[bbsId]/register/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsnormal/[bbsId]/register/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/(post)/bbsnormal/[bbsId]/register/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsnormal/[bbsId]/register/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsnotice/[bbsId]/[pstNo]/modify/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsnotice/[bbsId]/[pstNo]/modify/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/(post)/bbsnotice/[bbsId]/[pstNo]/modify/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsnotice/[bbsId]/[pstNo]/modify/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsnotice/[bbsId]/[pstNo]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsnotice/[bbsId]/[pstNo]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/(post)/bbsnotice/[bbsId]/[pstNo]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsnotice/[bbsId]/[pstNo]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsnotice/[bbsId]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsnotice/[bbsId]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/(post)/bbsnotice/[bbsId]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsnotice/[bbsId]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsnotice/[bbsId]/register/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsnotice/[bbsId]/register/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/(post)/bbsnotice/[bbsId]/register/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/(post)/bbsnotice/[bbsId]/register/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/register/[id]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/register/[id]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/register/[id]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/register/[id]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/register/page.tsx b/apps/cs/app/(main)/(with-base-layout)/bbs/register/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/bbs/register/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/bbs/register/page.tsx diff --git a/apps/cs/app/(with-base-layout)/board-statistics/page.tsx b/apps/cs/app/(main)/(with-base-layout)/board-statistics/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/board-statistics/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/board-statistics/page.tsx diff --git a/apps/cs/app/(with-base-layout)/code/page.tsx b/apps/cs/app/(main)/(with-base-layout)/code/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/code/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/code/page.tsx diff --git a/apps/cs/app/(with-base-layout)/contents/page.tsx b/apps/cs/app/(main)/(with-base-layout)/contents/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/contents/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/contents/page.tsx diff --git a/apps/cs/app/(with-base-layout)/contents/register/[sequential]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/contents/register/[sequential]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/contents/register/[sequential]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/contents/register/[sequential]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/contents/register/page.tsx b/apps/cs/app/(main)/(with-base-layout)/contents/register/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/contents/register/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/contents/register/page.tsx diff --git a/apps/cs/app/(with-base-layout)/dashboard/page.tsx b/apps/cs/app/(main)/(with-base-layout)/dashboard/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/dashboard/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/dashboard/page.tsx diff --git a/apps/cs/app/(with-base-layout)/file/page.tsx b/apps/cs/app/(main)/(with-base-layout)/file/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/file/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/file/page.tsx diff --git a/apps/cs/app/(with-base-layout)/groups/page.tsx b/apps/cs/app/(main)/(with-base-layout)/groups/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/groups/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/groups/page.tsx diff --git a/apps/cs/app/(with-base-layout)/image/page.tsx b/apps/cs/app/(main)/(with-base-layout)/image/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/image/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/image/page.tsx diff --git a/apps/cs/app/(with-base-layout)/image/register/[sequential]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/image/register/[sequential]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/image/register/[sequential]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/image/register/[sequential]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/image/register/page.tsx b/apps/cs/app/(main)/(with-base-layout)/image/register/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/image/register/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/image/register/page.tsx diff --git a/apps/cs/app/(with-base-layout)/layout.tsx b/apps/cs/app/(main)/(with-base-layout)/layout.tsx similarity index 91% rename from apps/cs/app/(with-base-layout)/layout.tsx rename to apps/cs/app/(main)/(with-base-layout)/layout.tsx index 3c1a527d7..acc583e2e 100644 --- a/apps/cs/app/(with-base-layout)/layout.tsx +++ b/apps/cs/app/(main)/(with-base-layout)/layout.tsx @@ -6,6 +6,7 @@ import { BaseLayout } from "@common/components"; import type { MenuItem } from "@/entities/menu"; import { useMenuStore } from "@/shared/lib"; import { PageLayout } from "@/shared/ui/page-layout"; +import { tokenManager } from "@/entities/auth"; export default function WithBaseLayout({ children, @@ -26,6 +27,7 @@ export default function WithBaseLayout({ return ( diff --git a/apps/cs/app/(with-base-layout)/link/page.tsx b/apps/cs/app/(main)/(with-base-layout)/link/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/link/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/link/page.tsx diff --git a/apps/cs/app/(with-base-layout)/link/register/[id]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/link/register/[id]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/link/register/[id]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/link/register/[id]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/link/register/page.tsx b/apps/cs/app/(main)/(with-base-layout)/link/register/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/link/register/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/link/register/page.tsx diff --git a/apps/cs/app/(with-base-layout)/linkLog/page.tsx b/apps/cs/app/(main)/(with-base-layout)/linkLog/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/linkLog/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/linkLog/page.tsx diff --git a/apps/cs/app/(with-base-layout)/linkParams/page.tsx b/apps/cs/app/(main)/(with-base-layout)/linkParams/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/linkParams/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/linkParams/page.tsx diff --git a/apps/cs/app/(with-base-layout)/linkStats/page.tsx b/apps/cs/app/(main)/(with-base-layout)/linkStats/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/linkStats/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/linkStats/page.tsx diff --git a/apps/cs/app/(with-base-layout)/linkTable/page.tsx b/apps/cs/app/(main)/(with-base-layout)/linkTable/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/linkTable/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/linkTable/page.tsx diff --git a/apps/cs/app/(with-base-layout)/loading.tsx b/apps/cs/app/(main)/(with-base-layout)/loading.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/loading.tsx rename to apps/cs/app/(main)/(with-base-layout)/loading.tsx diff --git a/apps/cs/app/(with-base-layout)/loginlog/page.tsx b/apps/cs/app/(main)/(with-base-layout)/loginlog/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/loginlog/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/loginlog/page.tsx diff --git a/apps/cs/app/(with-base-layout)/menu/page.tsx b/apps/cs/app/(main)/(with-base-layout)/menu/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/menu/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/menu/page.tsx diff --git a/apps/cs/app/(with-base-layout)/module/page.tsx b/apps/cs/app/(main)/(with-base-layout)/module/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/module/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/module/page.tsx diff --git a/apps/cs/app/(with-base-layout)/module/register/[sequential]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/module/register/[sequential]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/module/register/[sequential]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/module/register/[sequential]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/module/register/page.tsx b/apps/cs/app/(main)/(with-base-layout)/module/register/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/module/register/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/module/register/page.tsx diff --git a/apps/cs/app/(main)/(with-base-layout)/ntftalk-history/page.tsx b/apps/cs/app/(main)/(with-base-layout)/ntftalk-history/page.tsx new file mode 100644 index 000000000..3e2afeb35 --- /dev/null +++ b/apps/cs/app/(main)/(with-base-layout)/ntftalk-history/page.tsx @@ -0,0 +1,3 @@ +import { NtftalkHistoryPage } from "@/pages/ntftalk-history"; + +export default NtftalkHistoryPage; diff --git a/apps/cs/app/(main)/(with-base-layout)/ntftalk-temp/page.tsx b/apps/cs/app/(main)/(with-base-layout)/ntftalk-temp/page.tsx new file mode 100644 index 000000000..04ff3524a --- /dev/null +++ b/apps/cs/app/(main)/(with-base-layout)/ntftalk-temp/page.tsx @@ -0,0 +1,3 @@ +import { NtftalkTempPage } from "@/pages/ntftalk-temp"; + +export default NtftalkTempPage; diff --git a/apps/cs/app/(with-base-layout)/organization/page.tsx b/apps/cs/app/(main)/(with-base-layout)/organization/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/organization/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/organization/page.tsx diff --git a/apps/cs/app/(with-base-layout)/popup/page.tsx b/apps/cs/app/(main)/(with-base-layout)/popup/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/popup/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/popup/page.tsx diff --git a/apps/cs/app/(with-base-layout)/popup/register/[popupSn]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/popup/register/[popupSn]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/popup/register/[popupSn]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/popup/register/[popupSn]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/popup/register/page.tsx b/apps/cs/app/(main)/(with-base-layout)/popup/register/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/popup/register/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/popup/register/page.tsx diff --git a/apps/cs/app/(with-base-layout)/privacylog/page.tsx b/apps/cs/app/(main)/(with-base-layout)/privacylog/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/privacylog/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/privacylog/page.tsx diff --git a/apps/cs/app/(with-base-layout)/recent/page.tsx b/apps/cs/app/(main)/(with-base-layout)/recent/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/recent/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/recent/page.tsx diff --git a/apps/cs/app/(with-base-layout)/role/page.tsx b/apps/cs/app/(main)/(with-base-layout)/role/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/role/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/role/page.tsx diff --git a/apps/cs/app/(with-base-layout)/role/register/[roleId]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/role/register/[roleId]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/role/register/[roleId]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/role/register/[roleId]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/role/register/page.tsx b/apps/cs/app/(main)/(with-base-layout)/role/register/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/role/register/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/role/register/page.tsx diff --git a/apps/cs/app/(with-base-layout)/schedule/page.tsx b/apps/cs/app/(main)/(with-base-layout)/schedule/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/schedule/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/schedule/page.tsx diff --git a/apps/cs/app/(with-base-layout)/schedule/register/[schdlSn]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/schedule/register/[schdlSn]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/schedule/register/[schdlSn]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/schedule/register/[schdlSn]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/schedule/register/page.tsx b/apps/cs/app/(main)/(with-base-layout)/schedule/register/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/schedule/register/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/schedule/register/page.tsx diff --git a/apps/cs/app/(with-base-layout)/site/page.tsx b/apps/cs/app/(main)/(with-base-layout)/site/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/site/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/site/page.tsx diff --git a/apps/cs/app/(with-base-layout)/site/register/[id]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/site/register/[id]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/site/register/[id]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/site/register/[id]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/site/register/page.tsx b/apps/cs/app/(main)/(with-base-layout)/site/register/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/site/register/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/site/register/page.tsx diff --git a/apps/cs/app/(with-base-layout)/survey/[srvySn]/modify/page.tsx b/apps/cs/app/(main)/(with-base-layout)/survey/[srvySn]/modify/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/survey/[srvySn]/modify/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/survey/[srvySn]/modify/page.tsx diff --git a/apps/cs/app/(with-base-layout)/survey/[srvySn]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/survey/[srvySn]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/survey/[srvySn]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/survey/[srvySn]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/survey/page.tsx b/apps/cs/app/(main)/(with-base-layout)/survey/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/survey/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/survey/page.tsx diff --git a/apps/cs/app/(with-base-layout)/survey/register/page.tsx b/apps/cs/app/(main)/(with-base-layout)/survey/register/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/survey/register/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/survey/register/page.tsx diff --git a/apps/cs/app/(with-base-layout)/template/page.tsx b/apps/cs/app/(main)/(with-base-layout)/template/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/template/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/template/page.tsx diff --git a/apps/cs/app/(with-base-layout)/template/register/[sequential]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/template/register/[sequential]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/template/register/[sequential]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/template/register/[sequential]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/template/register/page.tsx b/apps/cs/app/(main)/(with-base-layout)/template/register/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/template/register/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/template/register/page.tsx diff --git a/apps/cs/app/(with-base-layout)/terms/page.tsx b/apps/cs/app/(main)/(with-base-layout)/terms/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/terms/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/terms/page.tsx diff --git a/apps/cs/app/(with-base-layout)/terms/register/[sequential]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/terms/register/[sequential]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/terms/register/[sequential]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/terms/register/[sequential]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/terms/register/page.tsx b/apps/cs/app/(main)/(with-base-layout)/terms/register/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/terms/register/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/terms/register/page.tsx diff --git a/apps/cs/app/(with-base-layout)/user-statistics/page.tsx b/apps/cs/app/(main)/(with-base-layout)/user-statistics/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/user-statistics/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/user-statistics/page.tsx diff --git a/apps/cs/app/(with-base-layout)/users/[userUnqId]/modify/page.tsx b/apps/cs/app/(main)/(with-base-layout)/users/[userUnqId]/modify/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/users/[userUnqId]/modify/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/users/[userUnqId]/modify/page.tsx diff --git a/apps/cs/app/(with-base-layout)/users/[userUnqId]/page.tsx b/apps/cs/app/(main)/(with-base-layout)/users/[userUnqId]/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/users/[userUnqId]/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/users/[userUnqId]/page.tsx diff --git a/apps/cs/app/(with-base-layout)/users/page.tsx b/apps/cs/app/(main)/(with-base-layout)/users/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/users/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/users/page.tsx diff --git a/apps/cs/app/(with-base-layout)/users/register/page.tsx b/apps/cs/app/(main)/(with-base-layout)/users/register/page.tsx similarity index 100% rename from apps/cs/app/(with-base-layout)/users/register/page.tsx rename to apps/cs/app/(main)/(with-base-layout)/users/register/page.tsx diff --git a/apps/cs/app/(without-base-layout)/view/popup/[popupSn]/page.tsx b/apps/cs/app/(main)/(without-base-layout)/view/popup/[popupSn]/page.tsx similarity index 100% rename from apps/cs/app/(without-base-layout)/view/popup/[popupSn]/page.tsx rename to apps/cs/app/(main)/(without-base-layout)/view/popup/[popupSn]/page.tsx diff --git a/apps/cs/app/(without-base-layout)/view/survey-article/[srvySn]/page.tsx b/apps/cs/app/(main)/(without-base-layout)/view/survey-article/[srvySn]/page.tsx similarity index 100% rename from apps/cs/app/(without-base-layout)/view/survey-article/[srvySn]/page.tsx rename to apps/cs/app/(main)/(without-base-layout)/view/survey-article/[srvySn]/page.tsx diff --git a/apps/cs/app/(without-base-layout)/view/survey-result/[srvySn]/page.tsx b/apps/cs/app/(main)/(without-base-layout)/view/survey-result/[srvySn]/page.tsx similarity index 100% rename from apps/cs/app/(without-base-layout)/view/survey-result/[srvySn]/page.tsx rename to apps/cs/app/(main)/(without-base-layout)/view/survey-result/[srvySn]/page.tsx diff --git a/apps/cs/app/layout.tsx b/apps/cs/app/(main)/layout.tsx similarity index 99% rename from apps/cs/app/layout.tsx rename to apps/cs/app/(main)/layout.tsx index aa2049dcc..d0810e7fe 100644 --- a/apps/cs/app/layout.tsx +++ b/apps/cs/app/(main)/layout.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next"; + import { App } from "@/app"; export const metadata: Metadata = { diff --git a/apps/cs/app/api/proxy/[...url]/route.ts b/apps/cs/app/api/proxy/[...url]/route.ts index abd3ae007..0929ca57c 100644 --- a/apps/cs/app/api/proxy/[...url]/route.ts +++ b/apps/cs/app/api/proxy/[...url]/route.ts @@ -2,6 +2,7 @@ import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, + COOKIE_DOMAIN, REFRESH_TOKEN, SERVER_API_URL, SITE_ID, @@ -60,18 +61,11 @@ async function handler( [AUTH_USER_ID]: userId, }); - const forwardedHost = request.headers.get("x-forwarded-host"); - const host = request.headers.get("host"); - const actualHost = forwardedHost || host || new URL(request.url).hostname.toString(); - - // 운영 환경에서 domain 설정 - const domain = actualHost.includes("hasuon.or.kr") ? ".hasuon.or.kr" : actualHost; - if (refreshToken) { - cookieStore.set(REFRESH_TOKEN, refreshToken, { + response.cookies.set(REFRESH_TOKEN, refreshToken, { httpOnly: true, sameSite: "strict", - domain: domain, + domain: getCookieDomain(), path: "/", }); } @@ -91,7 +85,6 @@ async function handler( } } - export async function POST( request: Request, props: { params: Promise<{ url: string[] }> }, @@ -107,3 +100,14 @@ export async function PUT( const params = await props.params; return handler(request, params, "PUT"); } + +function getCookieDomain(): string | undefined { + if (typeof window === "undefined") return undefined; + + if (COOKIE_DOMAIN) return COOKIE_DOMAIN; + + const { hostname } = window.location; + if (hostname === "localhost" || hostname === "127.0.0.1") return undefined; + + return hostname; +} diff --git a/apps/cs/docs/spec/ntftalk/history/SPEC.md b/apps/cs/docs/spec/ntftalk/history/SPEC.md new file mode 100644 index 000000000..51740470a --- /dev/null +++ b/apps/cs/docs/spec/ntftalk/history/SPEC.md @@ -0,0 +1,164 @@ +# 화면설계서: 알림톡발송이력관리 + +## 1. 기본 정보 + +| 항목 | 내용 | +| ---------- | -------------------------------------------- | +| 버전 | 1.0 | +| 프로세스명 | UI설계 | +| 화면명 | 알림톡발송이력관리 | +| 화면 유형 | [x] 조회 [ ] 입력 [ ] 삭제 [ ] 수정 [ ] 출력 | +| 메뉴 경로 | home > 시스템관리 > 알림톡발송이력관리 | + +--- + +## 2. 화면 목적 + +알림톡 발송 이력을 **기간·내용·수신자·결과·시스템** 조건으로 검색하고, **발송 시스템·알림 내용·수신자·수신 시각·성공 여부**를 목록으로 확인한다. + +--- + +## 3. 화면 구성 + +### 3-1. 전체 레이아웃 + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ 페이지 제목: 알림톡발송이력관리 브레드크럼 (우측 상단) │ +├────────────────────────────────────────────────────────────────────┤ +│ [영역 A] 검색 필터 │ +│ · 조회기간, 알림내용, 사용자, 결과, 시스템 │ +│ · [조회], [초기화] │ +├────────────────────────────────────────────────────────────────────┤ +│ [영역 B] 알림톡 발송 이력 그리드 │ +│ · 순번, 시스템, 알림내용, 수신자, 수신일, 결과 │ +│ · 페이지네이션 │ +└────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 4. 영역 A: 검색 필터 + +### 4-1. 필드 정의 + +| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 | +| -------- | -------------------- | ---- | ------------- | ----------------------------------- | ------------------------------------------- | +| 조회기간 | InputDateRangePicker | N | 1개월 전~오늘 | `dateFrom`, `dateTo` (`yyyy-MM-dd`) | `src/shared/ui/input-date-range-picker.tsx` | +| 알림내용 | 텍스트 | N | 공백 | `content` | | +| 사용자 | 텍스트 | N | 공백 | `recipientNum` | | +| 결과 | 드롭다운 | N | 전체 | `successYn` | `SUCCESS` \| `FAIL` \| `ALL` | +| 시스템 | SiteSelect | N | 전체 | `system` | `src/entities/site/ui/site-select.tsx` | + +### 4-2. 버튼 + +| 버튼명 | 동작 | +| ------ | ---------------------------------------------------------------- | +| 조회 | 현재 필터로 영역 B 목록을 재조회한다. 페이지는 1페이지로 초기화. | +| 초기화 | 검색 필드를 기본값으로 되돌린다. | + +--- + +## 5. 영역 B: 알림톡 발송 이력 그리드 + +### 5-1. 그리드 컬럼 + +| 표시명 | Field | 비고 | +| -------- | -------------- | ------------------------------------------------------------------------- | +| 순번 | — | 클라이언트 표시용 인덱스(서버 필드 없음). 페이지네이션 시 오프셋 반영 | +| 시스템 | `siteSystem` | 응답 시 `templateCode` 앞 4자 기반 파생(행정지원·하수도통계·포털·기타) | +| 알림내용 | `content` | 긴 텍스트 말줄임 처리. 툴팁 적용하여 전체 텍스트 확인 가능. | +| 수신자 | `recipientNum` | 실데이터·마스킹 규칙 **TBD** | +| 수신일 | `dateRslt` | `YYYY-MM-DD HH:mm:ss` 형식 표시. | +| 결과 | `successYn` | UI 표시: `SUCCESS` → 성공, `FAIL` → 실패(백엔드는 `reportCode` 기반 파생) | + +- 행 클릭 UX 없음. + +--- + +## 6. API/DTO 명세 + +### 6-1. 공통 + +- 목록 응답: Spring `Page` 직렬화 형태 — 프론트 타입 `PageableList` (`content`, `totalElements`, `totalPages`, `number`, `size` 등). +- 목록 요청 쿼리: `Pageable` 표준 파라미터 `page`, `size`, `sort` (`PageableParams`). + +### 6-2. 알림톡 발송 이력 API + +Base: `/ntftalk-service/api/v1/ata/mmt-log-history` + +| 기능 | Method | URL | Request DTO | Response DTO | API(클라이언트 함수명) | +| ----------------- | ------ | ----------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ----------------------------- | +| 목록 조회(페이징) | GET | `/ata/mmt-log-history` | Query: `AtaMmtLogHistorySearchDto` 필드 + `Pageable`(`page`,`size`,`sort`) | `Page` → `NtftalkMmtLogHistoryPageList` | `getNtftalkMmtLogHistoryPage` | +| 상세 조회 | GET | `/ata/mmt-log-history/{mtPr}` | Path: `mtPr` | `AtaMmtLogHistoryResponseDto` → `NtftalkMmtLogHistory` | `getNtftalkMmtLogHistory` | +| 등록 | POST | `/ata/mmt-log-history` | Body: `AtaMmtLogHistorySaveRequestDto` → `NtftalkMmtLogHistorySavePayload` | `AtaMmtLogHistoryResponseDto` | `saveNtftalkMmtLogHistory` | +| 수정 | PUT | `/ata/mmt-log-history/{mtPr}` | Path: `mtPr`, Body: `AtaMmtLogHistoryUpdateRequestDto` → `NtftalkMmtLogHistoryUpdatePayload` | `AtaMmtLogHistoryResponseDto` | `updateNtftalkMmtLogHistory` | +| 삭제 | DELETE | `/ata/mmt-log-history/{mtPr}` | Path: `mtPr` | 없음(204) | `deleteNtftalkMmtLogHistory` | + +#### 검색 Query ↔ TypeScript (`NtftalkMmtLogHistorySearchParams`) + +| Query (Java 필드) | TS 필드 | 설명 | +| ----------------- | --------------- | ------------------------------------ | +| `subject` | `subject?` | 메시지 제목 | +| `content` | `content?` | 전송 메시지 내용 | +| `recipientNum` | `recipientNum?` | 수신자 전화번호 | +| `msgStatus` | `msgStatus?` | `1` 전송대기, `2` 결과대기, `3` 완료 | +| `reportCode` | `reportCode?` | 전송 결과 코드(`1000`: 성공) | +| `dateFrom` | `dateFrom?` | `yyyy-MM-dd` | +| `dateTo` | `dateTo?` | `yyyy-MM-dd` | +| `system` | `system?` | `ADSS` \| `CWMS` \| `PORT` \| `ALL` | +| `successYn` | `successYn?` | `SUCCESS` \| `FAIL` \| `ALL` | + +#### 응답 행 ↔ TypeScript (`NtftalkMmtLogHistory`) + +백엔드 `AtaMmtLogHistoryResponseDto`와 동일 필드명: `mtPr`, `mtRefkey`, `priority`, `dateClientReq`, `subject`, `content`, `callback`, `msgStatus`, `recipientNum`, `dateMtSent`, `dateRslt`, `dateMtReport`, `reportCode`, `countryCode`, `msgType`, `regDate`, `senderKey`, `templateCode`, `siteSystem`, `successYn`. + +--- + +## 7. UI 구현 가이드 + +### 7-1. 기술 스택·원칙 + +- **UI**: `@common/components` 공통 컴포넌트 +- **테이블**: `@tanstack/react-table` + `useDataTable`(`src/shared/lib/use-data-table.tsx`) +- **데이터**: `@tanstack/react-query` — 목록은 `getNtftalkMmtLogHistoryPage`, 무한 스크롤 vs 페이지 UI는 **TBD** +- **레이아웃**: App Router `app/(with-base-layout)/ntftalk-history/page.tsx`에서 페이지 UI 마운트 + +### 7-2. 모듈·파일 배치 + +| 구분 | 경로 | +| --------------- | ------------------------------------------------------------------------------------------------------------------- | +| 라우트 엔트리 | `apps/cs/app/(with-base-layout)/ntftalk-history/page.tsx` | +| 페이지 셸 | `apps/cs/src/pages/ntftalk-history/ui/ntftalk-history-page.tsx` | +| 컬럼 정의 | `apps/cs/src/pages/ntftalk-history/model/*-table-columns.tsx` | +| API·타입 | `apps/cs/src/entities/ntftalk/model/ntftalk-mmt-log-history-interface.ts`, `api/ntftalk-mmt-log-history-service.ts` | +| 엔드포인트 상수 | `apps/cs/src/shared/config/api.ts` — `ENDPOINT.NTFTALK_SERVICE.MMT_LOG_HISTORY` | + +### 7-3. 영역별 UI 매핑 + +| 영역 | 화면 요소 | 컴포넌트·훅 | 구현 비고 | +| ---- | ------------ | ------------------------------------------------ | ---------------------------- | +| A | 검색 폼 | `Input`, `Label`, `Select`, `Button` | | +| B | 이력 그리드 | `useDataTable` + `ColumnDef`, `manualPagination` | | +| 공통 | 로딩·빈 목록 | `LoadingWithAnimation`, 빈 데이터 안내 | 기타 목록 페이지와 동일 수준 | + +### 7-4. 데이터 흐름 + +``` +BEGIN + + // 최초 진입 + CALL 알림톡발송이력목록조회(기본 검색 조건, page=0, size=10) + DISPLAY 알림톡 발송 이력 그리드 (영역 B) + + ON [조회] 버튼 CLICK DO + CALL 알림톡발송이력목록조회(조회기간→dateFrom/dateTo, content, recipientNum, successYn, system, page=0) + DISPLAY 알림톡 발송 이력 그리드 (영역 B) + END ON + + ON [초기화] 버튼 CLICK DO + RESET 검색 조건 → 기본값 + END ON + +END +``` diff --git a/apps/cs/docs/spec/ntftalk/temp/SPEC.md b/apps/cs/docs/spec/ntftalk/temp/SPEC.md new file mode 100644 index 000000000..81f361925 --- /dev/null +++ b/apps/cs/docs/spec/ntftalk/temp/SPEC.md @@ -0,0 +1,338 @@ +# 화면설계서: 알림톡템플릿관리 + +## 1. 기본 정보 + +| 항목 | 내용 | +| ---------- | -------------------------------------------- | +| 버전 | 1.0 | +| 프로세스명 | UI설계 | +| 화면명 | 알림톡템플릿관리 | +| 화면 유형 | [x] 조회 [x] 입력 [x] 삭제 [ ] 수정 [ ] 출력 | +| 메뉴 경로 | home > 시스템관리 > 알림톡템플릿관리 | + +--- + +## 2. 화면 목적 + +카카오 알림톡(또는 동일 채널) **템플릿 목록을 조회·관리**하고, 선택한 템플릿에 연결된 **치환 변수 목록**을 확인·관리한다. + +--- + +## 3. 화면 구성 + +### 3-1. 전체 레이아웃 + +``` +┌──────────────────────────────────────────────────────────────────────────────┐ +│ 페이지 제목: 알림톡템플릿관리 브레드크럼 (우측 상단) │ +├──────────────────────────────────────────────────────────────────────────────┤ +│ [영역 A] 검색 필터 │ +│ · 1행: 템플릿코드, 템플릿명, 템플릿내용 (텍스트) │ +│ · 2행: 채널, 검수상태, 메시지유형 (드롭다운, 기본값: 전체) | 조회, 초기화 │ +├──────────────────────────────────────────────────────────────────────────────┤ +│ [영역 B] 알림톡 템플릿 관리 │ +│ · 섹션 제목 + 툴바(템플릿갱신, 템플릿등록, 선택 검수요청, 선택삭제, 저장) │ +│ · 템플릿 그리드 (체크박스 다중 선택) │ +├──────────────────────────────────────────────────────────────────────────────┤ +│ [영역 C] 변수관리 │ +│ · 섹션 제목 + 툴바(변수추가, 선택삭제, 저장) │ +│ · 변수 그리드 │ +└──────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 4. 영역 A: 검색 필터 + +### 4-1. 필드 정의 + +| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 | +| ---------- | --------- | ---- | ------ | ------------------ | --------------------------------------- | +| 템플릿코드 | 텍스트 | N | 공백 | `templateCode` | | +| 템플릿명 | 텍스트 | N | 공백 | `templateName` | | +| 템플릿내용 | 텍스트 | N | 공백 | - | 현재 `NtftalkTempSearchParams`에 미정의 | +| 채널 | 드롭다운 | N | 전체 | `senderKey` | | +| 검수상태 | 드롭다운 | N | 전체 | `inspectionStatus` | | +| 메시지유형 | 드롭다운 | N | 전체 | - | 현재 `NtftalkTempSearchParams`에 미정의 | + +### 4-2. 버튼 + +| 버튼명 | 동작 | +| ------ | ----------------------------------------------------------------------- | +| 조회 | 현재 필터 조건으로 영역 B 목록을 재조회한다. 페이지는 1페이지로 초기화. | +| 초기화 | 검색 필드를 기본값(공백·전체)으로 되돌린다. | + +--- + +## 5. 영역 B: 알림톡 템플릿 관리 + +### 5-1. 툴바 버튼 + +| 버튼명 | 동작(요구사항) | +| ------------- | ------------------------------------------------------- | +| 템플릿갱신 | API를 재조회하여 템플릿 목록을 갱신한다. | +| 템플릿등록 | 신규 템플릿 등록 다이얼로그를 오픈한다. | +| 선택 검수요청 | 체크된 행에 대해 검수 요청 API를 호출한다. | +| 선택삭제 | 체크된 행을 삭제한다. 데이터베이스에는 삭제되지 않는다. | +| 저장 | 그리드에서 변경된 내용을 일괄 저장한다. | + +※ 실제 API·권한은 백엔드 정책에 따른다. + +### 5-2. 그리드 컬럼 + +| 표시명 | Field | 비고 | +| ---------- | ----------------------- | ----------------------------------------- | +| (선택) | — | 체크박스, 다중 선택 | +| @채널 | `senderKey` | | +| 템플릿코드 | `templateCode` | | +| 템플릿명 | `templateName` | | +| 메시지유형 | `templateMessageType` | | +| 템플릿내용 | `templateContent` | 긴 텍스트, 말줄임 처리 | +| 강조유형 | `templateEmphasizeType` | | +| 버튼정보 | `buttons` | | +| 검수상태 | `inspectionStatus` | | +| 상태 | `status` | | +| 등록일자 | `createdAt` | ISO datetime 문자열 (YYYY-MM-DD HH:mm:ss) | + +### 5-3. 행 상호작용 + +- 행 **클릭** 시: 해당 템플릿의 상세 다이얼로그를 오픈한다. +- 다중 체크는 **삭제·검수요청** 등 일괄 작업용으로 사용한다. + +--- + +## 6. 영역 C: 변수관리 + +알림톡 템플릿에 사용되는 공통 변수 목록을 관리한다. + +### 6-1. 툴바 버튼 + +| 버튼명 | 동작 | +| -------- | ----------------------------- | +| 변수추가 | 변수 행 추가(인라인). | +| 선택삭제 | 선택된 변수 행 삭제. | +| 저장 | 추가·수정·삭제 반영 API 호출. | + +### 6-2. 그리드 컬럼 + +| 표시명 | Field | 비고 | +| -------- | --------- | ----------------------------------------- | +| 순번 | — | 화면 표시용 row index(서버 필드 없음) | +| 변수코드 | `varCd` | | +| 변수명 | `varNm` | | +| 설명 | `varDesc` | | +| 등록일자 | `regDt` | ISO datetime 문자열 (YYYY-MM-DD HH:mm:ss) | +| 등록자 | `regr` | | + +--- + +## 7. API/DTO 명세 + +### 7-1. 공통 + +- Base Endpoint Prefix: `/ntftalk-service/api/v1` + +### 7-2. 템플릿 API + +Base: `ENDPOINT.NTFTALK_SERVICE.NTFTALK_TEMP`(`/ntftalk-service/api/v1/ntftalk-temp`) + +| 기능 | Method | URL | Request DTO | Response DTO | API | +| ---------------- | ------ | ------------------------------ | ------------------------------------------------------ | --------------------- | -------------------- | +| 템플릿 목록 조회 | GET | `/ntftalk-temp` | `NtftalkTempSearchParams` | `NtftalkTempPageList` | `getNtftalkTempPage` | +| 템플릿 상세 조회 | GET | `/ntftalk-temp/{templateCode}` | Path: `templateCode` | `NtftalkTemp` | `getNtftalkTemp` | +| 템플릿 등록 | POST | `/ntftalk-temp` | `NtftalkTempSavePayload` | `NtftalkTemp` | `saveNtftalkTemp` | +| 템플릿 수정 | PUT | `/ntftalk-temp/{templateCode}` | Path: `templateCode`, Body: `NtftalkTempUpdatePayload` | `NtftalkTemp` | `updateNtftalkTemp` | +| 템플릿 삭제 | DELETE | `/ntftalk-temp/{templateCode}` | Path: `templateCode` | 없음 | `deleteNtftalkTemp` | + +### 7-3. 변수 API + +Base: `ENDPOINT.NTFTALK_SERVICE.NTFTALK_VAR`(`/ntftalk-service/api/v1/ntftalk-var`) + +| 기능 | Method | URL | Request DTO | Response DTO | API | +| -------------- | ------ | ---------------------- | ---------------------------------------------- | -------------------- | ------------------- | +| 변수 목록 조회 | GET | `/ntftalk-var` | `NtftalkVarSearchParams` | `NtftalkVarPageList` | `getNtftalkVarPage` | +| 변수 상세 조회 | GET | `/ntftalk-var/{varCd}` | Path: `varCd` | `NtftalkVar` | `getNtftalkVar` | +| 변수 등록 | POST | `/ntftalk-var` | `NtftalkVarSavePayload` | `NtftalkVar` | `saveNtftalkVar` | +| 변수 수정 | PUT | `/ntftalk-var/{varCd}` | Path: `varCd`, Body: `NtftalkVarUpdatePayload` | `NtftalkVar` | `updateNtftalkVar` | +| 변수 삭제 | DELETE | `/ntftalk-var/{varCd}` | Path: `varCd` | 없음 | `deleteNtftalkVar` | + +--- + +## 8. UI 구현 가이드 + +### 8-1. 기술 스택·원칙 + +- **UI**: `@common/components` 공통 컴포넌트 사용 +- **테이블**: `@tanstack/react-table` + `useDataTable`(`src/shared/lib/use-data-table.tsx`) +- **데이터**: `@tanstack/react-query`(등록/수정 폼에 검증이 필요하면 `react-hook-form` + `zod` 사용) +- **레이아웃**: App Router `app/(with-base-layout)/.../page.tsx`에서 페이지 UI 컴포넌트만 마운트 + +### 8-2. 모듈·파일 배치 + +| 구분 | 경로 | +| ------------- | ----------------------------------------------------------------------- | +| 라우트 엔트리 | `apps/cs/app/(with-base-layout)/ntftalk-temp/page.tsx` | +| 페이지 셸 | `apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-page.tsx` | +| 컬럼 정의 | `apps/cs/src/pages/ntftalk-temp/model/*-table-columns.tsx` | +| API·타입 | `apps/cs/src/entities/ntftalk/model/*-interface.ts`, `api/*-service.ts` | +| 다이얼로그 | `apps/cs/src/pages/ntftalk-temp/ui/*-dialog.tsx` | + +### 8-3. 영역별 UI 매핑 + +| 영역 | 화면 요소 | 권장 컴포넌트·훅 | 구현 비고 | +| ---- | ------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------- | +| A | 검색 폼 | `Input`, `Label`, `Select`, `Button` | | +| B | 템플릿 그리드 | `useDataTable` + `ColumnDef`, `RowSelectionState`(컬럼 선택) | `manualPagination`, `onRowClick` → **8-4** 상세 다이얼로그 | +| B | 툴바 | `Button` 그룹 | 검수요청·삭제 등 확인은 `useConfirm` + `src/shared/ui/confirm-dialog.tsx` 등 기존 패턴 재사용 | +| C | 변수 그리드 | `useDataTable` + `ColumnDef`, `Input` + `zod` | **변수추가는 인라인**(6-1절). 선택 행 삭제·저장은 변수 API(7-3절)와 매핑. 검증은 `zod` 사용 | +| 공통 | 로딩·빈 목록 | `LoadingWithAnimation`, 빈 데이터 안내 문구 | 기타 목록 페이지와 동일 수준 처리 | + +### 8-4. 다이얼로그·라우팅 + +| UX | 구현 단위 | 컨테이너·비고 | +| --------------------------------- | ------------------------- | ---------------------------------------------------------------------------- | +| 템플릿등록 (5-1) | `NtftalkTempRegistDialog` | `Dialog` + 폼. `saveNtftalkTemp`(7-2) | +| 행 클릭 상세 (5-3) | `NtftalkTempDetailDialog` | **별도 라우트가 아닌 다이얼로그**로 구현. `getNtftalkTemp`(7-2)로 상세 로드 | +| 선택 검수요청·선택삭제·저장 (5-1) | mutation + confirm | 각각 대응 API(7-2) 및 백엔드 정책에 따름. 미확정 엔드포인트는 **TBD**로 표기 | + +### 8-5. 데이터 흐름 + +``` +BEGIN + + // 최초 진입 + CALL 템플릿목록조회(기본 검색 조건) + DISPLAY 알림톡템플릿그리드 (영역 B) + + CALL 변수목록조회(검색 조건) + DISPLAY 변수그리드 (영역 C) + + // 영역 A: 조회·초기화 + ON [조회] 버튼 CLICK DO + CALL 템플릿목록조회(템플릿코드, 템플릿명, 템플릿내용, 채널, 검수상태, 메시지유형) // → 영역 B 갱신, 페이지 1로 초기화 + END ON + + ON [초기화] 버튼 CLICK DO + RESET 검색 조건 → 기본값(공백·전체) + END ON + + // 영역 B: 알림톡 템플릿 (7-2절 참고) + ON [템플릿갱신] CLICK DO + CALL 템플릿목록조회(현재 검색 조건) // → 영역 B 갱신 + END ON + + ON [템플릿등록] CLICK DO + OPEN 템플릿등록 다이얼로그 + END ON + + ON 영역 B 행 CLICK DO + CALL 템플릿상세조회(선택된 templateCode) + DISPLAY 템플릿상세 다이얼로그 + END ON + + ON [선택 검수요청] CLICK DO + CONFIRM 사용자 + CALL 검수요청 API(체크된 행) + END ON + + ON [선택삭제] CLICK DO + CONFIRM 사용자 + CALL 템플릿삭제 API(체크된 행) + END ON + + ON [저장] CLICK DO + CALL 템플릿 일괄저장 API(그리드 변경분) + END ON + + // 영역 C: 변수 (7-3절 참고) + ON [변수추가] CLICK DO + ADD 변수 행 (인라인 편집) + END ON + + ON [선택삭제] CLICK DO (영역 C) + CONFIRM 사용자 + CALL 변수삭제 API(선택 행) + END ON + + ON [저장] CLICK DO (영역 C) + CALL 변수 등록·수정 API(변경분) + END ON + +END +``` + +--- + +## 9. 템플릿 등록 다이얼로그 + +대상 UI: `NtftalkTempRegistDialog` (`apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-regist-dialog.tsx`) + +### 9-1. 목적/오픈 조건 + +- 목적: 신규 알림톡 템플릿 1건을 등록한다. +- 오픈 조건: 영역 B 툴바의 `템플릿등록` 클릭 시 모달 오픈. +- 저장 성공 시: 목록 캐시 무효화 후 영역 B 재조회 상태로 복귀(모달 닫힘). + +### 9-2. 다이얼로그 레이아웃 + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ 알림 템플릿 등록 │ +├──────────────────────────────────────────────────────────────────────┤ +│ 메시지 유형 [기본형(고정)] │ +│ 채널 [Select(senderKey)] │ +│ 템플릿코드 [Input] │ +│ 템플릿명 [Input] │ +│ 보안템플릿여부 ( )설정 ( )미설정 │ +│ 템플릿내용 [+ 변수추가] │ +│ [Textarea] │ +│ 버튼 Table(버튼타입|버튼명|버튼링크) │ +│ [CodeSelect(upCd: NTFBUTTONTYPE) | Input | Input] │ +│ 바로연결 Table(바로연결타입|바로연결명|바로연결링크) │ +│ [CodeSelect(upCd: NTFQRTYPE) | Input | Input] │ +├──────────────────────────────────────────────────────────────────────┤ +│ [저장] [취소] │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +### 9-3. 필드 상세 + +| 표시명 | Form Key | 입력 유형 | 필수 | 기본값 | 검증/제약 | 저장 Payload 매핑 | +| -------------- | ----------------------- | ----------- | ---- | ------------------------------------- | -------------------- | --------------------- | +| 메시지 유형 | `templateMessageType` | 고정 텍스트 | Y | `BA` | UI에서 `기본형` 고정 | `templateMessageType` | +| 채널 | `senderKey` | Select | Y | `senderQuery.data.senderKey` 자동세팅 | 미선택 불가 | `senderKey` | +| 발신키 유형 | `senderKeyType` | hidden 상수 | Y | `S` | submit 시 강제 `S` | `senderKeyType` | +| 템플릿코드 | `templateCode` | Input | Y | `""` | 1자 이상 | `templateCode` | +| 템플릿명 | `templateName` | Input | Y | `""` | 1자 이상 | `templateName` | +| 보안템플릿여부 | `templateEmphasizeType` | Radio | N | `false` | `true` / `false` | `pcflag` | +| 템플릿내용 | `templateContent` | Textarea | Y | `""` | 1자 이상 | `templateContent` | +| 버튼 | `buttons` | Table | N | `[]` | 빈 배열 | `buttons` | +| 바로연결 | `quickReplies` | Table | N | `[]` | 빈 배열 | `quickReplies` | + +### 9-4. 버튼/이벤트 상세 + +| 트리거 | 처리 로직 | 비고 | +| ---------------- | ----------------------------------------------- | ------------------------------------ | +| 템플릿등록(외부) | 다이얼로그 open = true, form reset | 진입 시 초기화 | +| 저장 | zod 검증 통과 후 `saveNtftalkTemp` 호출 | 성공 시 `등록되었습니다.` 알림 | +| 저장 성공 | `invalidateQueries([NTFTALK_TEMP])` → 모달 닫기 | 목록 최신화 | +| 저장 실패 | 공통 mutation error 처리 | 에러 메시지 표시 정책 공통 모듈 따름 | +| + 변수추가 | 변수 선택 팝업 open | 9-5 참조 | + +### 9-5. 변수추가 팝업 상세 + +| 항목 | 내용 | +| ----------- | -------------------------------------------------------------------- | +| 팝업명 | 변수 목록 | +| 데이터 소스 | `NtftalkVarApi.getNtftalkVarPage` | +| 조회 조건 | `sort=regDt,desc`, 페이지네이션(`pageIndex/pageSize`) | +| 선택 방식 | 다중 선택 (`RowSelectionState`) | +| [추가] 동작 | 선택된 `varCd`를 줄바꿈(`\n`)으로 `templateContent` 하단에 append | +| 유효성 체크 | 선택 0건: "추가할 변수를 선택해 주세요." / `varCd` 공백 포함 시 에러 | +| [취소] 동작 | 팝업 닫기(선택값 유지 안 함) | + +### 9-6. API 명세 + +| 기능 | Method | URL | Request DTO | Response DTO | 클라이언트 API | +| ----------- | ------ | --------------- | ------------------------ | ------------- | -------------------------------- | +| 템플릿 등록 | POST | `/ntftalk-temp` | `NtftalkTempSavePayload` | `NtftalkTemp` | `NtftalkTempApi.saveNtftalkTemp` | diff --git a/apps/cs/eslint.config.mjs b/apps/cs/eslint.config.mjs index 9109635e9..c2643e6aa 100644 --- a/apps/cs/eslint.config.mjs +++ b/apps/cs/eslint.config.mjs @@ -1,6 +1,7 @@ import { dirname } from "path"; import { fileURLToPath } from "url"; import { FlatCompat } from "@eslint/eslintrc"; +import baseEsLintConfig from "../../eslint.config.mjs"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -11,9 +12,11 @@ const compat = new FlatCompat({ const eslintConfig = [ ...compat.extends("next/core-web-vitals", "next/typescript"), + ...baseEsLintConfig, { rules: { "react-hooks/exhaustive-deps": "off", + "@typescript-eslint/no-unused-vars": "warn", }, }, ]; diff --git a/apps/cs/middleware.ts b/apps/cs/middleware.ts index 3a79c1deb..98b741325 100644 --- a/apps/cs/middleware.ts +++ b/apps/cs/middleware.ts @@ -1,21 +1,25 @@ import { NextRequest, NextResponse } from "next/server"; + import { - CLAIM_NAME, AUTH_USER_ID, + CLAIM_NAME, PORT, PROXY_HOST, SERVER_API_URL, SITE_ID, } from "./src/shared/config"; -const serverApiUrl = - process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000"; +const serverApiUrl = process.env.SERVER_API_URL ?? "http://localhost:3000"; -const linkBackendApiHost = process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST ?? "http://localhost:8080"; +const linkBackendApiHost = + process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST ?? "http://localhost:8080"; // CSP 헤더 설정 함수 (img-src만) function addSecurityHeaders(response: NextResponse): NextResponse { - response.headers.set("Content-Security-Policy", "img-src 'self' data: https: blob:;"); + response.headers.set( + "Content-Security-Policy", + "img-src 'self' data: https: blob:;", + ); return response; } @@ -37,7 +41,7 @@ export function middleware(req: NextRequest) { logMessages.push(` Rewritten URL: ${newUrl}`); logMessages.push("\n--------------------------------\n"); - console.log(logMessages.join("\n")); + // console.log(logMessages.join("\n")); const response = NextResponse.rewrite(new URL(newUrl)); return addSecurityHeaders(response); @@ -54,7 +58,7 @@ export function middleware(req: NextRequest) { logMessages.push(` Rewritten URL: ${newUrl}`); logMessages.push("\n--------------------------------\n"); - console.log(logMessages.join("\n")); + // console.log(logMessages.join("\n")); const response = NextResponse.rewrite(new URL(newUrl), { headers: requestHeaders, @@ -66,7 +70,7 @@ export function middleware(req: NextRequest) { const requestHeaders = new Headers(req.headers); const token = req.cookies.get(CLAIM_NAME)?.value; const userId = req.cookies.get(AUTH_USER_ID)?.value; - + // /api/link 경로를 제거하고 백엔드 API 경로로 변환 const apiPath = pathname.replace("/api/link", ""); const queryParams = searchParams.toString(); @@ -79,7 +83,7 @@ export function middleware(req: NextRequest) { logMessages.push(` Link API Rewritten URL: ${newUrl}`); logMessages.push("\n--------------------------------\n"); - console.log(logMessages.join("\n")); + // console.log(logMessages.join("\n")); const response = NextResponse.rewrite(new URL(newUrl), { headers: requestHeaders, @@ -93,12 +97,17 @@ export function middleware(req: NextRequest) { } logMessages.push("\n--------------------------------\n"); - console.log(logMessages.join("\n")); - + // console.log(logMessages.join("\n")); + const response = NextResponse.next(); return addSecurityHeaders(response); } export const config = { - matcher: ["/server/:path*", "/attach-service/:path*", "/api/link/:path*", "/"], + matcher: [ + "/server/:path*", + "/attach-service/:path*", + "/api/link/:path*", + "/", + ], }; diff --git a/apps/cs/package.json b/apps/cs/package.json index 27f43fcc1..5bf3b53e7 100644 --- a/apps/cs/package.json +++ b/apps/cs/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@common/assets": "workspace:*", + "@common/auth": "workspace:*", "@common/components": "workspace:*", "@common/config": "workspace:*", "@fontsource-variable/noto-sans-kr": "^5.2.5", @@ -53,6 +54,7 @@ "@types/webpack": "^5.28.5", "eslint": "^9.0.0", "eslint-config-next": "15.3.8", + "eslint-plugin-react-hooks": "^5.2.0", "postcss": "^8.4.49", "prettier": "^3.4.2", "prettier-plugin-tailwindcss": "^0.6.11", @@ -60,4 +62,4 @@ "tailwindcss": "^3.4.14", "typescript": "^5.7.2" } -} \ No newline at end of file +} diff --git a/apps/cs/src/app/providers/app-provider.tsx b/apps/cs/src/app/providers/app-provider.tsx index efd1f308e..31d14ac79 100644 --- a/apps/cs/src/app/providers/app-provider.tsx +++ b/apps/cs/src/app/providers/app-provider.tsx @@ -14,8 +14,9 @@ import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; import { SelectPromptDialog } from "@/shared/ui/select-prompt-dialog"; import { PromptProvider, StoreProvider } from "@/shared/providers"; +const queryClient = new QueryClient(); + const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { - const queryClient = new QueryClient(); return ( diff --git a/apps/cs/src/entities/auth/api/auth-service.ts b/apps/cs/src/entities/auth/api/auth-service.ts new file mode 100644 index 000000000..e48569677 --- /dev/null +++ b/apps/cs/src/entities/auth/api/auth-service.ts @@ -0,0 +1,105 @@ +import { getRemainingMs } from "@common/auth"; +import { + ENDPOINT, + CLAIM_NAME, + ACCESS_TOKEN, + AUTH_USER_ID, +} from "@/shared/config"; +import type { ILogin } from "../model/auth-interface"; +import { + setToken, + clearAuthToken, + setupRefreshTimer, +} from "../lib/token-manager"; + +function resolveLoginError(message: string | undefined): string { + switch (message) { + case "Invalid Credentials": + return "아이디와 비밀번호가 일치하지 않습니다."; + case "User not found": + return "존재하지 않는 아이디입니다."; + case "User is not active": + return "활성화되지 않은 계정입니다."; + case "User is not verified": + return "이메일 인증이 필요합니다."; + default: + return message ?? "로그인에 실패했습니다."; + } +} + +export async function login(payload: ILogin): Promise { + const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(resolveLoginError(data?.message)); + } + + const accessToken = data[ACCESS_TOKEN] as string | undefined; + const tokenId = data[AUTH_USER_ID] as string | undefined; + + if (!accessToken || !tokenId) { + throw new Error("인증 토큰을 받지 못했습니다."); + } + + setToken(CLAIM_NAME, accessToken); + setToken(AUTH_USER_ID, tokenId); + + const remainingMs = getRemainingMs(accessToken); + if (remainingMs > 0) { + setupRefreshTimer(silentRefresh, remainingMs); + } +} + +export async function silentRefresh(): Promise { + console.log(`[silentRefresh] Called at ${new Date().toISOString()}`); + try { + const response = await fetch( + `/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`, + { method: "PUT" }, + ); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data?.message ?? "토큰 갱신에 실패했습니다."); + } + + const accessToken = data[ACCESS_TOKEN] as string | undefined; + const tokenId = data[AUTH_USER_ID] as string | undefined; + + if (!accessToken || !tokenId) { + throw new Error("갱신된 인증 토큰을 받지 못했습니다."); + } + + setToken(CLAIM_NAME, accessToken); + setToken(AUTH_USER_ID, tokenId); + + const remainingMs = getRemainingMs(accessToken); + if (remainingMs > 0) { + setupRefreshTimer(silentRefresh, remainingMs); + } + } catch (error) { + console.error( + `[silentRefresh] Failed at ${new Date().toISOString()}`, + error, + ); + clearAuthToken(); + if (typeof window !== "undefined") { + window.dispatchEvent(new CustomEvent("auth:logout")); + } + // re-throw하지 않음: setupRefreshTimer 콜백의 catch와 이중 처리 방지 + } +} + +export async function logout(): Promise { + clearAuthToken(); + if (typeof window !== "undefined") { + window.dispatchEvent(new CustomEvent("auth:logout")); + } +} diff --git a/apps/cs/src/entities/auth/api/login-service.ts b/apps/cs/src/entities/auth/api/login-service.ts deleted file mode 100644 index 671c8bb4c..000000000 --- a/apps/cs/src/entities/auth/api/login-service.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { AxiosError } from "axios"; -import { - ENDPOINT, - JWT_EXPIRATION_TIME, - ACCESS_TOKEN, - AUTH_USER_ID, - CLAIM_NAME, -} from "@/shared/config"; -import type { LoginService, ILogin } from "../model/login-interface"; -import { tokenManager } from "../lib/token-manager"; - -export class LoginApiService implements LoginService { - private static instance: LoginApiService; - - private constructor() {} - - public static getInstance(): LoginApiService { - if (!LoginApiService.instance) { - LoginApiService.instance = new LoginApiService(); - } - return LoginApiService.instance; - } - - public async login(payload: ILogin): Promise { - try { - const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, { - method: "POST", - body: JSON.stringify({ - userId: payload.userId, - password: payload.password, - }), - }); - - const data = await response.json(); - console.log('data ', data) - const accessToken = data[ACCESS_TOKEN]; - const tokenId = data[AUTH_USER_ID]; - - if (!accessToken || !tokenId) { - if (data.message == 'Invalid Credentials' ) { - throw new Error('아이디와 비밀번호가 일치하지 않습니다.'); - } else if (data.message == 'User not found') { - throw new Error('존재하지 않는 아이디입니다.'); - } else if (data.message == 'User is not active') { - throw new Error('활성화되지 않은 계정입니다.'); - } else if (data.message == 'User is not verified') { - throw new Error('이메일 인증이 필요합니다.'); - } else { - throw new Error(data.message); - } - } - - tokenManager.setToken(CLAIM_NAME, accessToken); - tokenManager.setToken(AUTH_USER_ID, tokenId); - tokenManager.setupRefreshTimer( - () => this.silentRefresh(), - JWT_EXPIRATION_TIME, - ); - } catch (error) { - if (error instanceof AxiosError) { - throw new Error(`Login 실패 ${error.response?.data?.message}`); - } - throw new Error(`Login 실패 ${error}`); - } - } - - public async silentRefresh(): Promise { - try { - const response = await fetch( - `/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`, - { method: "PUT" }, - ); - - const data = await response.json(); - const accessToken = data[ACCESS_TOKEN]; - const tokenId = data[AUTH_USER_ID]; - - if (!accessToken || !tokenId) { - throw new Error("Missing required authentication tokens"); - } - - tokenManager.setToken(CLAIM_NAME, accessToken); - tokenManager.setToken(AUTH_USER_ID, tokenId); - } catch (error) { - tokenManager.clearAuthToken(); - if (error instanceof AxiosError) { - throw new Error( - `Silent refresh failed: ${error.response?.data?.message}`, - ); - } - throw new Error(`Silent refresh failed: ${error}`); - } - } -} - -export const LoginApi = LoginApiService.getInstance(); diff --git a/apps/cs/src/entities/auth/index.ts b/apps/cs/src/entities/auth/index.ts index 97e142225..76b22ca39 100644 --- a/apps/cs/src/entities/auth/index.ts +++ b/apps/cs/src/entities/auth/index.ts @@ -1,2 +1,3 @@ -export { LoginApi } from "./api/login-service"; +export { login, silentRefresh, logout } from "./api/auth-service"; export { AuthWrapper } from "./lib/auth-wrapper"; +export { tokenManager } from "./lib/token-manager"; \ No newline at end of file diff --git a/apps/cs/src/entities/auth/lib/auth-wrapper.tsx b/apps/cs/src/entities/auth/lib/auth-wrapper.tsx index 57ae75c34..3c4dea9c7 100644 --- a/apps/cs/src/entities/auth/lib/auth-wrapper.tsx +++ b/apps/cs/src/entities/auth/lib/auth-wrapper.tsx @@ -1,47 +1,80 @@ "use client"; import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { isAxiosError } from "axios"; +import { getRemainingMs } from "@common/auth"; +import { isAtLoginRedirectUrl } from "@common/network"; import { client } from "@/shared/api"; import type { IUserInfo } from "@/shared/store"; -import { getCookie, useUserStore } from "@/shared/lib"; -import { isAtLoginRedirectUrl } from "@common/network"; +import { useUserStore } from "@/shared/lib"; import { ENDPOINT, AUTH_USER_ID, AUTH_LOGIN_REDIRECT_URL, + CLAIM_NAME, } from "@/shared/config"; +import { getToken, setupRefreshTimer } from "./token-manager"; +import { silentRefresh } from "../api/auth-service"; const AuthWrapper = ({ children }: { children: React.ReactNode }) => { - const { userInfo, setUserInfo } = useUserStore((state) => state); - - async function authCheck() { - if (isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) { - return; - } - - const userId = getCookie(AUTH_USER_ID); - if (!userId) { - throw new Error("Missing required authentication tokens"); - } - - const response = await client.get( - `${ENDPOINT.USER_SERVICE.USERS}/info/me`, - ); - if (!userInfo) setUserInfo(response.data); - } + const router = useRouter(); + const userInfo = useUserStore((state) => state.userInfo); + const setUserInfo = useUserStore((state) => state.setUserInfo); useEffect(() => { - (async () => { - try { - await authCheck(); - } catch (error) { - console.error(error); - setUserInfo(null); - if (!isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) { - window.location.assign(AUTH_LOGIN_REDIRECT_URL); - } + // 페이지 새로고침 시 silentRefresh 타이머 복구 + const token = getToken(CLAIM_NAME); + if (token) { + const remainingMs = getRemainingMs(token); + if (remainingMs > 0) { + console.log( + `[AuthWrapper] Refresh timer set: ${remainingMs}ms (${remainingMs / 1000}s)`, + ); + setupRefreshTimer(silentRefresh, remainingMs); } - })(); + } + + if (isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) return; + + async function authCheck() { + const userId = getToken(AUTH_USER_ID); + if (!userId) { + router.replace(AUTH_LOGIN_REDIRECT_URL); + return; + } + + if (userInfo) return; + + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}/info/me`, + ); + setUserInfo(response.data); + } + + authCheck().catch((error) => { + // 401은 @common/network error-interceptor에서 이미 리다이렉트 처리됨 + if (isAxiosError(error) && error.response?.status === 401) return; + + console.error(error); + setUserInfo(null); + if (!isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) { + router.replace(AUTH_LOGIN_REDIRECT_URL); + } + }); + }, []); + + // silentRefresh 실패 시 auth:logout 이벤트를 수신해 로그인으로 이동 + useEffect(() => { + function handleAuthLogout() { + setUserInfo(null); + if (!isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) { + router.replace(AUTH_LOGIN_REDIRECT_URL); + } + } + + window.addEventListener("auth:logout", handleAuthLogout); + return () => window.removeEventListener("auth:logout", handleAuthLogout); }, []); return <>{children}; diff --git a/apps/cs/src/entities/auth/lib/token-manager.ts b/apps/cs/src/entities/auth/lib/token-manager.ts index dd9aea352..44aa888aa 100644 --- a/apps/cs/src/entities/auth/lib/token-manager.ts +++ b/apps/cs/src/entities/auth/lib/token-manager.ts @@ -1,82 +1,15 @@ -import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config"; -import { getCookie, setCookie, removeCookie } from "@/shared/lib"; +import { createTokenManager } from "@common/auth"; +import { authConfig } from "@/shared/config/auth"; -export class TokenManager { - private static instance: TokenManager; - private refreshTimer: NodeJS.Timeout | null = null; +export const tokenManager = createTokenManager(authConfig); - private constructor() {} - - public static getInstance(): TokenManager { - if (!TokenManager.instance) { - TokenManager.instance = new TokenManager(); - } - return TokenManager.instance; - } - - public getToken(key: string): string | undefined { - return getCookie(key); - } - - public setToken(key: string, value: string): void { - const domain = this.getCookieDomain(); - - setCookie(key, value, { - secure: window.location.protocol === "https:", - sameSite: "lax", - domain: domain, - path: "/", // 모든 경로에서 접근 가능 - }); - } - - - private getCookieDomain(): string | undefined { - const hostname = window.location.hostname; - - // 개발 환경에서는 localhost 도메인 사용 - if (hostname === "localhost" || hostname === "127.0.0.1") { - return undefined; // localhost에서는 도메인 설정하지 않음 - } - - // 운영 환경에서는 서브도메인을 포함한 최상위 도메인 사용 - if (hostname.includes("hasuon.or.kr")) { - return ".hasuon.or.kr"; - } - - // 기타 환경에서는 현재 호스트명 사용 - return hostname; - } - - public removeToken(key: string): void { - const domain = this.getCookieDomain(); - removeCookie(key, { domain, path: "/" }); - } - - public setupRefreshTimer( - callback: () => Promise, - expirationTime: number, - ): void { - if (this.refreshTimer) { - clearTimeout(this.refreshTimer); - } - - this.refreshTimer = setTimeout(() => { - callback().catch((error) => { - console.error("Silent refresh failed:", error); - this.clearAuthToken(); - }); - }, expirationTime - 60000); - } - - public clearAuthToken(): void { - if (this.refreshTimer) { - clearTimeout(this.refreshTimer); - this.refreshTimer = null; - } - - this.removeToken(CLAIM_NAME); - this.removeToken(AUTH_USER_ID); - } -} - -export const tokenManager = TokenManager.getInstance(); +export const { + getToken, + setToken, + removeToken, + setupRefreshTimer, + clearAuthToken, + getAccessToken, + getRefreshToken, + getUserId, +} = tokenManager; diff --git a/apps/cs/src/entities/auth/model/auth-interface.ts b/apps/cs/src/entities/auth/model/auth-interface.ts new file mode 100644 index 000000000..012f81457 --- /dev/null +++ b/apps/cs/src/entities/auth/model/auth-interface.ts @@ -0,0 +1,4 @@ +export interface ILogin { + userId: string; + password: string; +} diff --git a/apps/cs/src/entities/auth/model/login-interface.ts b/apps/cs/src/entities/auth/model/login-interface.ts deleted file mode 100644 index 5aaf53044..000000000 --- a/apps/cs/src/entities/auth/model/login-interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface ILogin { - userId: string; - password: string; -} - -export interface LoginService { - login(payload: ILogin): Promise; - silentRefresh(): Promise; -} diff --git a/apps/cs/src/entities/code/model/lwpocode-interface.ts b/apps/cs/src/entities/code/model/lwpocode-interface.ts index 2538c1c30..51137294a 100644 --- a/apps/cs/src/entities/code/model/lwpocode-interface.ts +++ b/apps/cs/src/entities/code/model/lwpocode-interface.ts @@ -1,5 +1,6 @@ import { z } from "zod"; -import type { PageableParams, PageableList } from "@/shared/model"; + +import type { PageableList, PageableParams } from "@/shared/model"; export interface LwpoCodeParams extends PageableParams { upCd?: string; @@ -36,15 +37,14 @@ export interface LwpoCodeDetail extends LwpoCodeFormData { } export const LwpoCodeFormSchema = z.object({ - lwpoCd: z.string().max(10).nonempty(), - upCd: z.string().max(10).nonempty(), + lwpoCd: z.string().max(30).nonempty(), + upCd: z.string().max(30).nonempty(), lwpoCdNm: z.string().max(100).nonempty(), lwpoCdExpln: z.string().max(1000).nullish(), - unit: z.string().max(20).nullish(), + unit: z.string().max(50).nullish(), prntLwpoCd: z.string().max(10).nullish(), useYn: z.string(), sortSeq: z.number(), - delYn: z.string(), }); export type LwpoCodeFormData = z.infer; diff --git a/apps/cs/src/entities/code/model/upcode-interface.ts b/apps/cs/src/entities/code/model/upcode-interface.ts index 73bf4f3ae..3738a5d0e 100644 --- a/apps/cs/src/entities/code/model/upcode-interface.ts +++ b/apps/cs/src/entities/code/model/upcode-interface.ts @@ -32,10 +32,13 @@ export interface UpCodeDetail extends UpCodeFormData { } export const upCodeFormSchema = z.object({ - upCd: z.string().max(10).nonempty(), - upCdNm: z.string().max(100).nonempty(), + upCd: z + .string() + .max(30, "최대 30자까지 입력할 수 있습니다.") + .nonempty("최소 1자 이상 입력해주세요."), + upCdNm: z.string().max(100).nonempty("최소 1자 이상 입력해주세요."), upCdExpln: z.string().max(1000).nullish(), - cdSe: z.string().max(10).nonempty(), + cdSe: z.string().max(30).nonempty(), siteId: z.string().max(2).nullish(), useYn: z.string(), delYn: z.string(), diff --git a/apps/cs/src/entities/editor/model/editor-base-config.ts b/apps/cs/src/entities/editor/model/editor-base-config.ts index ee465b675..f0684c5c8 100644 --- a/apps/cs/src/entities/editor/model/editor-base-config.ts +++ b/apps/cs/src/entities/editor/model/editor-base-config.ts @@ -1,4 +1,4 @@ -import { SERVER_API_URL } from "@/shared/config"; +import { TINY_BASE_URL } from "@/shared/config"; export const editorBaseConfig = { // 폐쇄망 환경에서는 API 키 없이 자체 호스팅 모드 사용 @@ -7,7 +7,7 @@ export const editorBaseConfig = { licenseKey: "gpl", init: { language: "ko_KR", - language_url: '/tinymce/langs/ko_KR.js', // 직접 호스팅한 경우 경로 지정 + language_url: "/tinymce/langs/ko_KR.js", // 직접 호스팅한 경우 경로 지정 height: 500, // 폐쇄망 환경을 위한 설정 base_url: "/tinymce", @@ -38,7 +38,7 @@ export const editorBaseConfig = { "undo redo | blocks fontfamily fontsize | bold italic underline strikethrough forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist blockquote | table", "link image", ], - images_upload_base_path: SERVER_API_URL, + images_upload_base_path: TINY_BASE_URL, file_picker_types: "image", // 폐쇄망 환경을 위한 추가 설정 content_css: "/tinymce/skins/content/default/content.min.css", diff --git a/apps/cs/src/entities/ntftalk/api/ntftalk-biztalk-service.ts b/apps/cs/src/entities/ntftalk/api/ntftalk-biztalk-service.ts new file mode 100644 index 000000000..85f585360 --- /dev/null +++ b/apps/cs/src/entities/ntftalk/api/ntftalk-biztalk-service.ts @@ -0,0 +1,64 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import { + type NtftalkBiztalkCheckInspectionResponse, + type NtftalkBiztalkInspectionRequestItem, + type NtftalkBiztalkInspectionResponse, + type NtftalkBiztalkSender, + type NtftalkBiztalkService, + type NtftalkBiztalkSyncTemplatesParams, + type NtftalkBiztalkSyncTemplatesResponse, +} from "../model/ntftalk-biztalk-interface"; + +export const NtftalkBiztalkApi: NtftalkBiztalkService = { + async getSender(): Promise { + const response = await client.get( + `${ENDPOINT.NTFTALK_SERVICE.NTFTALK_BIZTALK}/sender`, + ); + return response.data; + }, + + async syncTemplates( + params?: NtftalkBiztalkSyncTemplatesParams, + ): Promise { + const response = await client.post( + `${ENDPOINT.NTFTALK_SERVICE.NTFTALK_BIZTALK}/sync-templates`, + undefined, + { + params: { + senderKeyType: params?.senderKeyType, + }, + }, + ); + return response.data; + }, + + async requestInspection( + requestList: NtftalkBiztalkInspectionRequestItem[], + ): Promise { + const response = await client.post( + `${ENDPOINT.NTFTALK_SERVICE.NTFTALK_BIZTALK}/request-inspection`, + requestList, + ); + return response.data; + }, + + async cancelInspection( + requestList: NtftalkBiztalkInspectionRequestItem[], + ): Promise { + const response = await client.post( + `${ENDPOINT.NTFTALK_SERVICE.NTFTALK_BIZTALK}/cancel-inspection`, + requestList, + ); + return response.data; + }, + + async checkInspection( + templateCode: string, + ): Promise { + const response = await client.get( + `${ENDPOINT.NTFTALK_SERVICE.NTFTALK_BIZTALK}/check-inspection/${encodeURIComponent(templateCode)}`, + ); + return response.data; + }, +}; diff --git a/apps/cs/src/entities/ntftalk/api/ntftalk-mmt-log-history-service.ts b/apps/cs/src/entities/ntftalk/api/ntftalk-mmt-log-history-service.ts new file mode 100644 index 000000000..ebfd90bdf --- /dev/null +++ b/apps/cs/src/entities/ntftalk/api/ntftalk-mmt-log-history-service.ts @@ -0,0 +1,58 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import { + type NtftalkMmtLogHistory, + type NtftalkMmtLogHistoryPageList, + type NtftalkMmtLogHistorySavePayload, + type NtftalkMmtLogHistorySearchParams, + type NtftalkMmtLogHistoryService, + type NtftalkMmtLogHistoryUpdatePayload, +} from "../model/ntftalk-mmt-log-history-interface"; + +class NtftalkMmtLogHistoryApiService implements NtftalkMmtLogHistoryService { + async getNtftalkMmtLogHistoryPage( + params: NtftalkMmtLogHistorySearchParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.NTFTALK_SERVICE.MMT_LOG_HISTORY}`, + { params }, + ); + return response.data; + } + + async getNtftalkMmtLogHistory(mtPr: number): Promise { + const response = await client.get( + `${ENDPOINT.NTFTALK_SERVICE.MMT_LOG_HISTORY}/${mtPr}`, + ); + return response.data; + } + + async saveNtftalkMmtLogHistory( + payload: NtftalkMmtLogHistorySavePayload, + ): Promise { + const response = await client.post( + `${ENDPOINT.NTFTALK_SERVICE.MMT_LOG_HISTORY}`, + payload, + ); + return response.data; + } + + async updateNtftalkMmtLogHistory( + mtPr: number, + payload: NtftalkMmtLogHistoryUpdatePayload, + ): Promise { + const response = await client.put( + `${ENDPOINT.NTFTALK_SERVICE.MMT_LOG_HISTORY}/${mtPr}`, + payload, + ); + return response.data; + } + + async deleteNtftalkMmtLogHistory(mtPr: number): Promise { + await client.delete( + `${ENDPOINT.NTFTALK_SERVICE.MMT_LOG_HISTORY}/${mtPr}`, + ); + } +} + +export const NtftalkMmtLogHistoryApi = new NtftalkMmtLogHistoryApiService(); diff --git a/apps/cs/src/entities/ntftalk/api/ntftalk-temp-service.ts b/apps/cs/src/entities/ntftalk/api/ntftalk-temp-service.ts new file mode 100644 index 000000000..91ccf6561 --- /dev/null +++ b/apps/cs/src/entities/ntftalk/api/ntftalk-temp-service.ts @@ -0,0 +1,64 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; + +import { + type NtftalkTemp, + type NtftalkTempDeleteMultiPayload, + type NtftalkTempPageList, + type NtftalkTempSavePayload, + type NtftalkTempSearchParams, + type NtftalkTempService, + type NtftalkTempUpdatePayload, +} from "../model/ntftalk-temp-interface"; + +class NtftalkTempApiService implements NtftalkTempService { + async getNtftalkTempPage( + params: NtftalkTempSearchParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.NTFTALK_SERVICE.NTFTALK_TEMP}`, + { params }, + ); + return response.data; + } + + async getNtftalkTemp(templateCode: string): Promise { + const response = await client.get( + `${ENDPOINT.NTFTALK_SERVICE.NTFTALK_TEMP}/${encodeURIComponent(templateCode)}`, + ); + return response.data; + } + + async saveNtftalkTemp(payload: NtftalkTempSavePayload): Promise { + const response = await client.post( + `${ENDPOINT.NTFTALK_SERVICE.NTFTALK_TEMP}`, + payload, + ); + return response.data; + } + + async updateNtftalkTemp( + templateCode: string, + payload: NtftalkTempUpdatePayload, + ): Promise { + const response = await client.put( + `${ENDPOINT.NTFTALK_SERVICE.NTFTALK_TEMP}/${encodeURIComponent(templateCode)}`, + payload, + ); + return response.data; + } + + async deleteNtftalkTemp(templateCode: string): Promise { + await client.delete( + `${ENDPOINT.NTFTALK_SERVICE.NTFTALK_TEMP}/${encodeURIComponent(templateCode)}`, + ); + } + + async deleteMultiNtftalkTemp( + payload: NtftalkTempDeleteMultiPayload, + ): Promise { + await client.post(`${ENDPOINT.NTFTALK_SERVICE.NTFTALK_TEMP}/delmulti`, payload); + } +} + +export const NtftalkTempApi = new NtftalkTempApiService(); diff --git a/apps/cs/src/entities/ntftalk/api/ntftalk-var-service.ts b/apps/cs/src/entities/ntftalk/api/ntftalk-var-service.ts new file mode 100644 index 000000000..88adfd477 --- /dev/null +++ b/apps/cs/src/entities/ntftalk/api/ntftalk-var-service.ts @@ -0,0 +1,59 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; + +import { + type NtftalkVar, + type NtftalkVarPageList, + type NtftalkVarSavePayload, + type NtftalkVarSearchParams, + type NtftalkVarService, + type NtftalkVarUpdatePayload, +} from "../model/ntftalk-var-interface"; + +class NtftalkVarApiService implements NtftalkVarService { + async getNtftalkVarPage( + params: NtftalkVarSearchParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.NTFTALK_SERVICE.NTFTALK_VAR}`, + { params }, + ); + return response.data; + } + + async getNtftalkVar(varCd: string): Promise { + const response = await client.get( + `${ENDPOINT.NTFTALK_SERVICE.NTFTALK_VAR}/${encodeURIComponent(varCd)}`, + ); + return response.data; + } + + async saveNtftalkVar( + payload: NtftalkVarSavePayload[], + ): Promise { + const response = await client.post( + `${ENDPOINT.NTFTALK_SERVICE.NTFTALK_VAR}`, + payload, + ); + return response.data; + } + + async updateNtftalkVar( + varCd: string, + payload: NtftalkVarUpdatePayload, + ): Promise { + const response = await client.put( + `${ENDPOINT.NTFTALK_SERVICE.NTFTALK_VAR}/${encodeURIComponent(varCd)}`, + payload, + ); + return response.data; + } + + async deleteNtftalkVar(varCd: string): Promise { + await client.delete( + `${ENDPOINT.NTFTALK_SERVICE.NTFTALK_VAR}/${encodeURIComponent(varCd)}`, + ); + } +} + +export const NtftalkVarApi = new NtftalkVarApiService(); diff --git a/apps/cs/src/entities/ntftalk/index.ts b/apps/cs/src/entities/ntftalk/index.ts new file mode 100644 index 000000000..0210af8a4 --- /dev/null +++ b/apps/cs/src/entities/ntftalk/index.ts @@ -0,0 +1,37 @@ +export { NtftalkVarApi } from "./api/ntftalk-var-service"; +export { NtftalkTempApi } from "./api/ntftalk-temp-service"; +export { NtftalkBiztalkApi } from "./api/ntftalk-biztalk-service"; +export { NtftalkMmtLogHistoryApi } from "./api/ntftalk-mmt-log-history-service"; +export type { + NtftalkVar, + NtftalkVarPageList, + NtftalkVarSavePayload, + NtftalkVarSearchParams, + NtftalkVarUpdatePayload, +} from "./model/ntftalk-var-interface"; +export type { + NtftalkBiztalkCheckInspectionResponse, + NtftalkBiztalkInspectionRequestItem, + NtftalkBiztalkInspectionResponse, + NtftalkBiztalkSender, + NtftalkBiztalkSyncTemplatesParams, + NtftalkBiztalkSyncTemplatesResponse, +} from "./model/ntftalk-biztalk-interface"; +export type { + NtftalkBtnTemp, + NtftalkBtnTempSavePayload, + NtftalkQuickTemp, + NtftalkQuickTempSavePayload, + NtftalkTemp, + NtftalkTempPageList, + NtftalkTempSavePayload, + NtftalkTempSearchParams, + NtftalkTempUpdatePayload, +} from "./model/ntftalk-temp-interface"; +export type { + NtftalkMmtLogHistory, + NtftalkMmtLogHistoryPageList, + NtftalkMmtLogHistorySavePayload, + NtftalkMmtLogHistorySearchParams, + NtftalkMmtLogHistoryUpdatePayload, +} from "./model/ntftalk-mmt-log-history-interface"; diff --git a/apps/cs/src/entities/ntftalk/model/ntftalk-biztalk-interface.ts b/apps/cs/src/entities/ntftalk/model/ntftalk-biztalk-interface.ts new file mode 100644 index 000000000..824eb00d5 --- /dev/null +++ b/apps/cs/src/entities/ntftalk/model/ntftalk-biztalk-interface.ts @@ -0,0 +1,48 @@ +export interface NtftalkBiztalkSender { + senderKey: string; + name: string; + uuid: string; + status: string; +} + +export interface NtftalkBiztalkSyncTemplatesParams { + senderKeyType?: string; +} + +export interface NtftalkBiztalkSyncTemplatesResponse { + syncCount: number; + message: string; +} + +export interface NtftalkBiztalkInspectionRequestItem { + senderKey: string; + templateCode: string; + senderKeyType: string; +} + +export interface NtftalkBiztalkInspectionResponse { + processedCount: number; + message: string; +} + +export interface NtftalkBiztalkCheckInspectionResponse { + templateCode: string; + approved: boolean; + message: string; +} + +export interface NtftalkBiztalkService { + getSender(): Promise; + syncTemplates( + params?: NtftalkBiztalkSyncTemplatesParams, + ): Promise; + requestInspection( + requestList: NtftalkBiztalkInspectionRequestItem[], + ): Promise; + cancelInspection( + requestList: NtftalkBiztalkInspectionRequestItem[], + ): Promise; + checkInspection( + templateCode: string, + ): Promise; +} diff --git a/apps/cs/src/entities/ntftalk/model/ntftalk-mmt-log-history-interface.ts b/apps/cs/src/entities/ntftalk/model/ntftalk-mmt-log-history-interface.ts new file mode 100644 index 000000000..9f272d1ac --- /dev/null +++ b/apps/cs/src/entities/ntftalk/model/ntftalk-mmt-log-history-interface.ts @@ -0,0 +1,84 @@ +import type { PageableList, PageableParams } from "@/shared/model"; + +export interface NtftalkMmtLogHistorySearchParams extends PageableParams { + subject?: string; + content?: string; + recipientNum?: string; + /** 1:전송대기, 2:결과대기, 3:완료 */ + msgStatus?: string; + /** 1000:성공 */ + reportCode?: string; + /** yyyy-MM-dd */ + dateFrom?: string; + /** yyyy-MM-dd */ + dateTo?: string; + /** ADSS | CWMS | PORT | ALL */ + system?: string; + /** SUCCESS | FAIL | ALL */ + successYn?: string; +} + +export interface NtftalkMmtLogHistory { + mtPr: number; + mtRefkey?: string | null; + priority?: string | null; + dateClientReq?: string | null; + subject?: string | null; + content?: string | null; + callback?: string | null; + msgStatus?: string | null; + recipientNum?: string | null; + dateMtSent?: string | null; + dateRslt?: string | null; + dateMtReport?: string | null; + reportCode?: string | null; + countryCode?: string | null; + msgType?: number | null; + regDate?: string | null; + senderKey?: string | null; + templateCode?: string | null; + siteSystem?: string | null; + successYn?: string | null; +} + +export interface NtftalkMmtLogHistorySavePayload { + mtPr: number; + mtRefkey?: string; + priority?: string; + dateClientReq?: string; + subject: string; + content: string; + callback: string; + msgStatus?: string; + recipientNum: string; + reportCode?: string; + countryCode?: string; + msgType?: number; + senderKey: string; + templateCode?: string; + responseMethod?: string; +} + +export interface NtftalkMmtLogHistoryUpdatePayload { + content: string; + recipientNum: string; + msgStatus?: string; + reportCode?: string; +} + +export type NtftalkMmtLogHistoryPageList = PageableList; + +export interface NtftalkMmtLogHistoryService { + getNtftalkMmtLogHistoryPage( + params: NtftalkMmtLogHistorySearchParams, + ): Promise; + getNtftalkMmtLogHistory(mtPr: number): Promise; + saveNtftalkMmtLogHistory( + payload: NtftalkMmtLogHistorySavePayload, + ): Promise; + updateNtftalkMmtLogHistory( + mtPr: number, + payload: NtftalkMmtLogHistoryUpdatePayload, + ): Promise; + deleteNtftalkMmtLogHistory(mtPr: number): Promise; +} diff --git a/apps/cs/src/entities/ntftalk/model/ntftalk-temp-interface.ts b/apps/cs/src/entities/ntftalk/model/ntftalk-temp-interface.ts new file mode 100644 index 000000000..5149ad230 --- /dev/null +++ b/apps/cs/src/entities/ntftalk/model/ntftalk-temp-interface.ts @@ -0,0 +1,122 @@ +import type { PageableList, PageableParams } from "@/shared/model"; + +export interface NtftalkTempSearchParams extends PageableParams { + templateCode?: string; + templateName?: string; + /** 백엔드 스펙 확정 시 전달 */ + templateContent?: string; + inspectionStatus?: string; + senderKey?: string; + status?: string; + /** 백엔드 스펙 확정 시 전달 */ + templateMessageType?: string; +} + +export interface NtftalkBtnTemp { + templateCode: string; + name: string; + ordering: number; + linkType: string; + linkMo: string; + linkPc: string; + linkAnd: string; + linkIos: string; +} + +export interface NtftalkQuickTemp { + templateCode: string; + name: string; + linkType: string; + linkMo: string; + linkPc: string; + linkAnd: string; + linkIos: string; +} + +export interface NtftalkTemp { + templateCode: string; + senderKey: string; + senderKeyType: string; + templateName: string; + templateContent: string; + templateMessageType: string; + templateMessageTypeNm: string; + templateEmphasizeType: string; + templateEmphasizeTypeNm: string; + templateImageUrl: string; + inspectionStatus: string; + inspectionStatusNm: string; + pcflag: string; + status: string; + statusNm: string; + createdAt: string; + modifiedAt: string; + buttons?: NtftalkBtnTemp[]; + quickReplies?: NtftalkQuickTemp[]; +} + +export interface NtftalkBtnTempSavePayload { + templateCode?: string; + name: string; + ordering: number; + linkType: string; + linkMo?: string; + linkPc?: string; + linkAnd?: string; + linkIos?: string; +} + +export interface NtftalkQuickTempSavePayload { + templateCode?: string; + name: string; + linkType: string; + linkMo?: string; + linkPc?: string; + linkAnd?: string; + linkIos?: string; +} + +export interface NtftalkTempSavePayload { + templateCode: string; + senderKey: string; + senderKeyType: string; + templateName: string; + templateContent: string; + templateMessageType: string; + templateEmphasizeType: string; + templateImageUrl?: string; + inspectionStatus?: string; + pcflag?: string; + buttons?: NtftalkBtnTempSavePayload[]; + quickReplies?: NtftalkQuickTempSavePayload[]; +} + +export interface NtftalkTempUpdatePayload { + templateName: string; + templateContent: string; + inspectionStatus?: string; + status?: string; + pcflag?: string; + buttons?: NtftalkBtnTempSavePayload[]; + quickReplies?: NtftalkQuickTempSavePayload[]; +} + +export interface NtftalkTempDeleteMultiPayload { + templateCodes: string[]; +} + +export type NtftalkTempPageList = PageableList; + +export interface NtftalkTempService { + getNtftalkTempPage( + params: NtftalkTempSearchParams, + ): Promise; + getNtftalkTemp(templateCode: string): Promise; + saveNtftalkTemp(payload: NtftalkTempSavePayload): Promise; + updateNtftalkTemp( + templateCode: string, + payload: NtftalkTempUpdatePayload, + ): Promise; + deleteNtftalkTemp(templateCode: string): Promise; + deleteMultiNtftalkTemp(payload: NtftalkTempDeleteMultiPayload): Promise; +} diff --git a/apps/cs/src/entities/ntftalk/model/ntftalk-var-interface.ts b/apps/cs/src/entities/ntftalk/model/ntftalk-var-interface.ts new file mode 100644 index 000000000..7bfc1d0e6 --- /dev/null +++ b/apps/cs/src/entities/ntftalk/model/ntftalk-var-interface.ts @@ -0,0 +1,56 @@ +import type { PageableList, PageableParams } from "@/shared/model"; + +export interface NtftalkVarSearchParams extends PageableParams { + varCd?: string; + varNm?: string; + regr?: string; +} + +export interface NtftalkVar { + varCd: string; + sender: string; + varNm: string; + varDesc: string; + regDt?: string; + regr: string; +} + +export type NtftalkVarSavePayload = + | { + mode: "C"; + varCd: string; + varNm: string; + sender?: string; + varDesc?: string; + regr?: string; + } + | { + mode: "U"; + varCd: string; + varNm: string; + varDesc?: string; + } + | { + mode: "D"; + varCd: string; + }; + +export interface NtftalkVarUpdatePayload { + varNm: string; + varDesc?: string; +} + +export type NtftalkVarPageList = PageableList; + +export interface NtftalkVarService { + getNtftalkVarPage( + params: NtftalkVarSearchParams, + ): Promise; + getNtftalkVar(varCd: string): Promise; + saveNtftalkVar(payload: NtftalkVarSavePayload[]): Promise; + updateNtftalkVar( + varCd: string, + payload: NtftalkVarUpdatePayload, + ): Promise; + deleteNtftalkVar(varCd: string): Promise; +} diff --git a/apps/cs/src/entities/site/api/site-service.ts b/apps/cs/src/entities/site/api/site-service.ts index a5d9d7cd6..342816d72 100644 --- a/apps/cs/src/entities/site/api/site-service.ts +++ b/apps/cs/src/entities/site/api/site-service.ts @@ -1,12 +1,13 @@ import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; + import { - siteFormSchema, - type SiteService, - type SiteList, type SiteDetail, - type SiteParams, type SiteFormData, + siteFormSchema, + type SiteList, + type SiteParams, + type SiteService, } from "../model/site-interface"; export class SiteApiService implements SiteService { @@ -36,9 +37,10 @@ export class SiteApiService implements SiteService { public async updateSite(siteId: string, data: SiteFormData): Promise { const validatedData = siteFormSchema.parse(data); + const { siteId: _, ...rest } = validatedData; const response = await client.put( `${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`, - validatedData, + rest, ); return response.data; } diff --git a/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx b/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx index f23a70663..1953ea763 100644 --- a/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx +++ b/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx @@ -101,7 +101,7 @@ function FaqPage({ bbsId }: { bbsId: string }) { alert("삭제되었습니다."); }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); refetch(); }; diff --git a/apps/cs/src/pages/admin/ui/add-admin-dialog.tsx b/apps/cs/src/pages/admin/ui/add-admin-dialog.tsx index c49d75530..5cebb8dfb 100644 --- a/apps/cs/src/pages/admin/ui/add-admin-dialog.tsx +++ b/apps/cs/src/pages/admin/ui/add-admin-dialog.tsx @@ -174,7 +174,7 @@ function AddAdminDialog() { } }; - const handleSearch = (e: React.FormEvent) => { + const handleSearch = (e: React.SubmitEvent) => { e.preventDefault(); e.stopPropagation(); setPagination({ pageIndex: 0, pageSize: 10 }); diff --git a/apps/cs/src/pages/admin/ui/admin-page.tsx b/apps/cs/src/pages/admin/ui/admin-page.tsx index c2d3aa5af..6a621b865 100644 --- a/apps/cs/src/pages/admin/ui/admin-page.tsx +++ b/apps/cs/src/pages/admin/ui/admin-page.tsx @@ -125,7 +125,7 @@ function AdminPage() { }, }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, diff --git a/apps/cs/src/pages/apilog/ui/apilog-page.tsx b/apps/cs/src/pages/apilog/ui/apilog-page.tsx index ab98a1931..b2c273005 100644 --- a/apps/cs/src/pages/apilog/ui/apilog-page.tsx +++ b/apps/cs/src/pages/apilog/ui/apilog-page.tsx @@ -75,7 +75,7 @@ function ApiLogPage() { }, }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, diff --git a/apps/cs/src/pages/auth/login/ui/login-page.tsx b/apps/cs/src/pages/auth/login/ui/login-page.tsx index fd1e2f809..fa7b84185 100644 --- a/apps/cs/src/pages/auth/login/ui/login-page.tsx +++ b/apps/cs/src/pages/auth/login/ui/login-page.tsx @@ -3,13 +3,13 @@ import { useState } from "react"; import Image from "next/image"; import { images } from "@common/assets"; -import { LoginApi } from "@/entities/auth"; +import { login } from "@/entities/auth"; import "./login.css"; function LoginPage() { const [errorMessage, setErrorMessage] = useState(""); - const handleSubmit = async (e: React.FormEvent) => { + const handleSubmit = async (e: React.SubmitEvent) => { e.preventDefault(); setErrorMessage(""); // 에러 메시지 초기화 @@ -18,7 +18,7 @@ function LoginPage() { const password = formData.get("password") as string; try { - await LoginApi.login({ userId, password }); + await login({ userId, password }); window.location.href = "/dashboard"; } catch (error) { const message = @@ -31,12 +31,12 @@ function LoginPage() {
-

- 하수도정보시스템 +

+ 유역하수도통합플랫폼
- CMS 통합관리시스템 + 하수도플랫폼 통합관리(CMS)시스템

-

하수도정보시스템 CMS 통합관리시스템에 오신 것을 환영합니다.

+

유역하수도통합플랫폼 통합관리(CMS)시스템에 오신 것을 환영합니다.

logo diff --git a/apps/cs/src/pages/banner/ui/banner-page.tsx b/apps/cs/src/pages/banner/ui/banner-page.tsx index 81435f010..e09f5ab58 100644 --- a/apps/cs/src/pages/banner/ui/banner-page.tsx +++ b/apps/cs/src/pages/banner/ui/banner-page.tsx @@ -48,7 +48,7 @@ function BannerPage() { }), }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, diff --git a/apps/cs/src/pages/bbs/ui/bbs-page.tsx b/apps/cs/src/pages/bbs/ui/bbs-page.tsx index 509f83cb7..abb1ec9db 100644 --- a/apps/cs/src/pages/bbs/ui/bbs-page.tsx +++ b/apps/cs/src/pages/bbs/ui/bbs-page.tsx @@ -51,7 +51,7 @@ function BbsPage() { }), }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, diff --git a/apps/cs/src/pages/board-statistics/ui/board-statistics-page.tsx b/apps/cs/src/pages/board-statistics/ui/board-statistics-page.tsx index 716c390ab..495f5d9f6 100644 --- a/apps/cs/src/pages/board-statistics/ui/board-statistics-page.tsx +++ b/apps/cs/src/pages/board-statistics/ui/board-statistics-page.tsx @@ -107,7 +107,7 @@ function BoardStatisticsPage() { setPresetDateRange(""); }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); const params: BoardStatisticsParams = { siteId, diff --git a/apps/cs/src/pages/code/ui/lwpocode-dialog.tsx b/apps/cs/src/pages/code/ui/lwpocode-dialog.tsx index 3179303dd..69dd31aa2 100644 --- a/apps/cs/src/pages/code/ui/lwpocode-dialog.tsx +++ b/apps/cs/src/pages/code/ui/lwpocode-dialog.tsx @@ -103,7 +103,6 @@ const LwpoCodeManagementDialog = memo( prntLwpoCd: detailData?.prntLwpoCd ?? "", useYn: detailData?.useYn ?? "Y", sortSeq: detailData?.sortSeq ?? 0, - delYn: "N", }, }); diff --git a/apps/cs/src/pages/code/ui/lwpocode-section.tsx b/apps/cs/src/pages/code/ui/lwpocode-section.tsx index 61726a6b8..fb8fcd91c 100644 --- a/apps/cs/src/pages/code/ui/lwpocode-section.tsx +++ b/apps/cs/src/pages/code/ui/lwpocode-section.tsx @@ -65,7 +65,7 @@ function LwpoCodeSection({ upCd }: LwpoCodeSectionProps) { dataTableOptions: { viewPagination: true }, }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, diff --git a/apps/cs/src/pages/code/ui/upcode-dialog.tsx b/apps/cs/src/pages/code/ui/upcode-dialog.tsx index 6774bd17e..d54030cca 100644 --- a/apps/cs/src/pages/code/ui/upcode-dialog.tsx +++ b/apps/cs/src/pages/code/ui/upcode-dialog.tsx @@ -31,7 +31,13 @@ import { import { SiteSelect } from "@/entities/site"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore, useMutationWrapper } from "@/shared/lib"; -import { Form, FormField, FormItem, FormControl } from "@/shared/ui/form"; +import { + Form, + FormField, + FormItem, + FormControl, + FormMessage, +} from "@/shared/ui/form"; type UpCodeManagementDialogProps = { upCd?: string; @@ -139,6 +145,7 @@ const UpCodeManagementDialog = memo( disabled={!isRegistration} /> + )} /> @@ -160,6 +167,7 @@ const UpCodeManagementDialog = memo( placeholder="상위코드명을 입력해주세요" /> + )} /> diff --git a/apps/cs/src/pages/code/ui/upcode-section.tsx b/apps/cs/src/pages/code/ui/upcode-section.tsx index ef89d2d3a..cd67725cf 100644 --- a/apps/cs/src/pages/code/ui/upcode-section.tsx +++ b/apps/cs/src/pages/code/ui/upcode-section.tsx @@ -40,7 +40,7 @@ function UpCodeSection({ handleSelectedUpCd }: UpCodeSectionProps) { handleSelectedUpCd(row.upCd); }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, diff --git a/apps/cs/src/pages/contents/ui/contents-page.tsx b/apps/cs/src/pages/contents/ui/contents-page.tsx index 51ba5c7c0..105aecdba 100644 --- a/apps/cs/src/pages/contents/ui/contents-page.tsx +++ b/apps/cs/src/pages/contents/ui/contents-page.tsx @@ -38,7 +38,7 @@ function ContentsPage() { placeholderData: keepPreviousData, }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, diff --git a/apps/cs/src/pages/file/ui/file-page.tsx b/apps/cs/src/pages/file/ui/file-page.tsx index 8b5eba617..f4ff3f97a 100644 --- a/apps/cs/src/pages/file/ui/file-page.tsx +++ b/apps/cs/src/pages/file/ui/file-page.tsx @@ -65,7 +65,7 @@ function FilePage() { }, }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, diff --git a/apps/cs/src/pages/groups/ui/add-user-dialog.tsx b/apps/cs/src/pages/groups/ui/add-user-dialog.tsx index 4f8a205ff..f2a1b68a2 100644 --- a/apps/cs/src/pages/groups/ui/add-user-dialog.tsx +++ b/apps/cs/src/pages/groups/ui/add-user-dialog.tsx @@ -149,7 +149,7 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) { } }; - const handleSearch = (e: React.FormEvent) => { + const handleSearch = (e: React.SubmitEvent) => { e.preventDefault(); e.stopPropagation(); setPagination({ diff --git a/apps/cs/src/pages/groups/ui/group-users-section.tsx b/apps/cs/src/pages/groups/ui/group-users-section.tsx index 4b7170e3a..db8b5dec3 100644 --- a/apps/cs/src/pages/groups/ui/group-users-section.tsx +++ b/apps/cs/src/pages/groups/ui/group-users-section.tsx @@ -12,7 +12,7 @@ import { type RowSelectionState, } from "@tanstack/react-table"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; -import { Button,Input, Label } from "@common/components"; +import { Button, Input, Label } from "@common/components"; import { GroupUsersApi } from "@/entities/group-user"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore, useDataTable } from "@/shared/lib"; @@ -33,8 +33,8 @@ function GroupUsersSection({ groupId }: GroupUsersSectionProps) { pageSize: 10, }); - const [userNm, setUserNm] = useState(""); - + const [userNm, setUserNm] = useState(""); + const [rowSelection, setRowSelection] = useState({}); const query = useQuery({ @@ -50,7 +50,7 @@ function GroupUsersSection({ groupId }: GroupUsersSectionProps) { placeholderData: keepPreviousData, }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, @@ -116,27 +116,27 @@ function GroupUsersSection({ groupId }: GroupUsersSectionProps) { return (
-
- - setUserNm(e.target.value)} - /> - - -
+
+ + setUserNm(e.target.value)} + /> + + +
diff --git a/apps/cs/src/pages/groups/ui/groups-dialog.tsx b/apps/cs/src/pages/groups/ui/groups-dialog.tsx index 9fca4ebdb..ac7f92916 100644 --- a/apps/cs/src/pages/groups/ui/groups-dialog.tsx +++ b/apps/cs/src/pages/groups/ui/groups-dialog.tsx @@ -43,7 +43,7 @@ const GroupsDialog = memo( const { data: group } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.GROUPS, groupId], queryFn: () => GroupApi.findGroupById(groupId!), - enabled: !!groupId && open, // open 시점 groupId가 있을 때만 상세 조회 API 호출 + enabled: !isRegistration, }); const { mutateAsync: createGroup } = useMutationWrapper({ @@ -92,8 +92,9 @@ const GroupsDialog = memo( }; useEffect(() => { - if (open) form.reset(); - }, [open]); + // 그룹 등록 및 취소 시 폼 초기화 + if (isRegistration && !open) form.reset(); + }, [groupId, open]); return ( 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/groups/ui/role-select.tsx b/apps/cs/src/pages/groups/ui/role-select.tsx index 539888591..bede34ed8 100644 --- a/apps/cs/src/pages/groups/ui/role-select.tsx +++ b/apps/cs/src/pages/groups/ui/role-select.tsx @@ -16,7 +16,13 @@ type RoleSelectProps = React.ComponentPropsWithoutRef & { function RoleSelect({ mngrYn = "N", ...props }: RoleSelectProps) { const { data: roles } = useQuery({ queryKey: [ENDPOINT.USER_SERVICE.ROLES], - queryFn: () => RoleApi.getRoles(), + queryFn: () => + RoleApi.getRoles({ + keywordType: "useYn", + keyword: "Y", + page: 0, + size: 100, + }), }); const options = roles?.content.filter((role) => role.mngrYn === mngrYn); 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-additional-schema.ts b/apps/cs/src/pages/ntftalk-temp/model/ntftalk-var-additional-schema.ts new file mode 100644 index 000000000..e2368fe77 --- /dev/null +++ b/apps/cs/src/pages/ntftalk-temp/model/ntftalk-var-additional-schema.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; + +export const ntftalkVarAdditionalSchema = z.object({ + draftVars: z.array( + z.object({ + varCd: z.string().min(1, "변수코드를 입력하세요."), + varNm: z.string().min(1, "변수명을 입력하세요."), + varDesc: z.string().default(""), + sender: z.string().default(""), + regr: z.string().default(""), + }), + ), +}); + +export type NtftalkVarAdditionalFormData = z.infer< + typeof ntftalkVarAdditionalSchema +>; diff --git a/apps/cs/src/pages/ntftalk-temp/model/ntftalk-var-batch-payload.ts b/apps/cs/src/pages/ntftalk-temp/model/ntftalk-var-batch-payload.ts new file mode 100644 index 000000000..91d715188 --- /dev/null +++ b/apps/cs/src/pages/ntftalk-temp/model/ntftalk-var-batch-payload.ts @@ -0,0 +1,52 @@ +import { z } from "zod"; + +import type { NtftalkVarSavePayload } from "@/entities/ntftalk"; + +import type { NtftalkVarGridRow } from "./ntftalk-var-grid"; + +export const varDraftRowSchema = z.object({ + varCd: z.string().min(1), + varNm: z.string().min(1), + varDesc: z.string().optional(), + sender: z.string().optional(), +}); + +export function buildNtftalkVarBatchPayload(input: { + pendingDeleteVarCds: ReadonlySet; + draftVars?: NtftalkVarGridRow[]; + regr?: string; +}): NtftalkVarSavePayload[] { + const { draftVars, pendingDeleteVarCds, regr } = input; + const payloads: NtftalkVarSavePayload[] = []; + + if (!draftVars) { + for (const varCd of pendingDeleteVarCds) { + payloads.push({ mode: "D", varCd }); + } + return payloads; + } + + for (const d of draftVars) { + const parsed = varDraftRowSchema.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 ?? "유효하지 않은 입력입니다.", + ); + } + payloads.push({ + mode: "C", + varCd: parsed.data.varCd, + varNm: parsed.data.varNm, + varDesc: parsed.data.varDesc, + sender: parsed.data.sender, + regr, + }); + } + + return payloads; +} 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..914130e09 --- /dev/null +++ b/apps/cs/src/pages/ntftalk-temp/model/ntftalk-var-grid.ts @@ -0,0 +1,17 @@ +import type { NtftalkVar } from "@/entities/ntftalk"; + +export interface NtftalkVarGridRow extends NtftalkVar { + clientId?: string; +} + +export function createEmptyVarDraft(clientId: string): NtftalkVarGridRow { + return { + clientId, + 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..e5d8a00fe --- /dev/null +++ b/apps/cs/src/pages/ntftalk-temp/model/template-table-columns.tsx @@ -0,0 +1,128 @@ +import { TooltipContent } from "@radix-ui/react-tooltip"; +import type { ColumnDef, Row, Table } from "@tanstack/react-table"; + +import { InfoTooltip } from "@/shared/ui/info-tooltip"; +import type { NtftalkBiztalkSender, NtftalkTemp } from "@/entities/ntftalk"; +import { Checkbox, Tooltip, TooltipTrigger } from "@common/components"; + +function formatButtons(row: NtftalkTemp): string { + if (!row.buttons?.length) return "—"; + return row.buttons.map((b) => b.name).join(", "); +} + +export function createTemplateColumns( + senderData: NtftalkBiztalkSender[], +): 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: "@채널", + cell: ({ row }) => { + const sender = senderData.find( + (s) => s.senderKey === row.original.senderKey, + ); + return sender ? sender.name : "—"; + }, + }, + { + accessorKey: "templateCode", + header: "템플릿코드", + }, + { + accessorKey: "templateName", + header: "템플릿명", + }, + { + accessorKey: "templateMessageTypeNm", + header: "메시지유형", + }, + { + accessorKey: "templateContent", + header: "템플릿내용", + cell: ({ getValue }) => { + const v = getValue() as string; + if (!v) return "—"; + return ( +
+ + +

+ {v} +

+
+ +

+ {v.split("\n").map((line, idx) => ( + + {line} + {idx !== v.split("\n").length - 1 &&
} +
+ ))} +

+
+
+
+ ); + }, + }, + { + accessorKey: "templateEmphasizeTypeNm", + header: "강조유형", + }, + { + id: "buttons", + header: "버튼정보", + cell: ({ row }) => formatButtons(row.original), + }, + { + accessorKey: "inspectionStatusNm", + header: "검수상태", + }, + { + accessorKey: "statusNm", + 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..19e02e78d --- /dev/null +++ b/apps/cs/src/pages/ntftalk-temp/model/var-table-columns.tsx @@ -0,0 +1,84 @@ +import type { ColumnDef, Row, Table } from "@tanstack/react-table"; + +import { Checkbox } 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(): 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: "변수코드", + }, + { + accessorKey: "varNm", + header: "변수명", + }, + { + accessorKey: "varDesc", + header: "설명", + }, + { + 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..ac2cbfc6f --- /dev/null +++ b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-page.tsx @@ -0,0 +1,15 @@ +"use client"; + +import { NtftalkTempTemplateSection } from "./ntftalk-temp-template-section"; +import { NtftalkVarSection } from "./ntftalk-temp-var-section"; + +function NtftalkTempPage() { + return ( +
+ + +
+ ); +} + +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..f989a0165 --- /dev/null +++ b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-regist-dialog.tsx @@ -0,0 +1,435 @@ +"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 { tableComponent, tableInstance } = useDataTable({ + query: varQuery, + tableOptions: { + columns: createVarColumns(), + 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 ( + + + + 알림 템플릿 등록 + +
+ + + + + + + + + + 메시지 유형 + + 기본형 + + + + 채널 + + + ( + + + + + + + )} + /> + + + + + 템플릿코드 + + + ( + + + + + + + )} + /> + + + + + 템플릿명 + + + ( + + + + + + + )} + /> + + + + + 보안템플릿여부 + + + ( + + + +
+ + +
+
+ + +
+
+
+ +
+ )} + /> +
+
+ + + 템플릿내용 + + + ( + +
+ +
+ +