Compare commits

..
Author SHA1 Message Date
leedj312 ca9f2da83c 수요정보 등록 수질 입력 부분 validation check 2025-08-01 14:38:38 +09:00
leedj312 e54a0d0ef8 수요신청현황 이상/이하 작업 완료 2025-08-01 11:37:23 +09:00
leedj312 82e29ec4e2 Merge commit '6808430c9ae74890d0f0e84f20a0ffe5409de157' into iipbs/dm/dev 2025-08-01 09:43:41 +09:00
jyha00 96a50e4f5a 수요처 로그인 비밀번호 초기화 요청 cursor-pointer 추가 2025-07-31 20:01:25 +09:00
jyha00 96448b6427 수요처 로그인 시 비밀번호 초기화 요청 추가 2025-07-31 17:50:05 +09:00
leedj312 0ef6aea502 Merge commit '33ad044ed34fe78bfe832c09c1128385a3fc285e' into iipbs/dm/dev 2025-07-31 15:42:36 +09:00
jyha00 33ad044ed3 대민 수정사항 반영 2025-07-31 15:01:36 +09:00
leedj312 c94b33b1b8 대민 신청현황 매칭된 공급처 보이도록 수정 2025-07-31 11:05:05 +09:00
leedj312 886561535e 대민 수요신청 정보 변경 2025-07-30 17:57:03 +09:00
leedj312 ebde6d433a 대민 수요처 등록후 매인으로 이동 2025-07-28 10:26:40 +09:00
yschoi 556d3f33b6 Merge branch 'iipbs/dm/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/dm/dev 2025-07-28 10:19:05 +09:00
yschoi f826f0fb3f env 2025-07-28 10:18:49 +09:00
leedj312 11010099b4 저장시 원래 화면으로 이동 2025-07-28 10:11:55 +09:00
yschoi 17886b0f9d Merge branch 'iipbs/dm/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/dm/dev 2025-07-28 09:46:53 +09:00
yschoi 46c166a87c 개발 서버 2025-07-28 09:46:00 +09:00
yschoi 5b96245e7d 빌드오류 수정 2025-07-28 09:25:54 +09:00
yschoi 6ba78d0923 빌드 오류 수정 2025-07-28 08:56:16 +09:00
yschoi 15b6520ee1 if (typeof window !== "undefined") {
tokenManager.setToken("site-id-key", 'DM');
      tokenManager.setToken("site-theme-key", 'green');
    }
2025-07-28 08:04:58 +09:00
yschoi ed53ad249c 후보지 선택 버튼 삭제 2025-07-28 07:52:40 +09:00
yschoi b67d1261ff 로컬 환경변수 2025-07-28 07:50:19 +09:00
yschoi cfde5d695f 환경변수 2025-07-28 07:35:02 +09:00
yschoi 13ce985fe5 1 2025-07-28 06:38:50 +09:00
yschoi 3e634fa575 개발 환경변수 수정 2025-07-28 06:29:44 +09:00
yschoi 16b204f83b 후보지 선택 2025-07-28 04:38:09 +09:00
yschoi 0f9d7a115f Merge remote-tracking branch 'origin/main' into iipbs/dm/dev 2025-07-27 14:29:31 +09:00
yschoi 05802896a8 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2025-07-27 14:27:32 +09:00
yschoi a44e8722c6 gis 수정 2025-07-27 14:27:23 +09:00
swcho 690e671a25 Merge remote-tracking branch 'origin/iipbs/sn/ii' into iipbs/sn/dev 2025-07-27 14:02:14 +09:00
swcho 46b98b99e2 fix: 운영정보 검색 결과 창에서 설치일자 형식 수정 2025-07-27 13:47:18 +09:00
swcho 9e339cd451 fix, feat:
- 빗물펌프 자체 입력 정보 '처리구역', '처리분구' -> '배수구역', '배수분구'으로 수정
- 운영정보 검색 화면에 일련번호 추가
2025-07-27 13:41:00 +09:00
leedj312 95085cbdeb 초기화 함수만 추가 2025-07-27 08:34:38 +09:00
yschoi c1b0f32609 화면 크기 조정 2025-07-27 00:19:12 +09:00
yschoi f9b328278a Merge remote-tracking branch 'origin/iipbs/wr/dev' 2025-07-27 00:06:24 +09:00
yschoi 8a5b9ba2c3 gis 목록 에서 피처 선택시 오류 수정 2025-07-27 00:00:54 +09:00
yschoi 763e0ff5d8 Merge remote-tracking branch 'origin/iipbs/wr/dev' 2025-07-26 20:57:02 +09:00
yschoi 93e8ec1df3 시설물 bbox 전략 수정 2025-07-26 20:51:56 +09:00
leedj312 f087e55c3c 중복되는 스토어 함수 2025-07-26 20:14:37 +09:00
yschoi 53b3cc9817 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2025-07-26 20:09:49 +09:00
yschoi b2bbdb8ad4 검색 화면 화면 조정 2025-07-26 20:09:41 +09:00
leedj312 417a24bb16 Merge commit '6fb39d638495b9510302ffd819cbd8ecd7a16dab' into iipbs/dm/dev 2025-07-26 19:40:20 +09:00
leedj312 72c1c01e47 Merge commit '6fb39d638495b9510302ffd819cbd8ecd7a16dab' into iipbs/wr/dev 2025-07-26 19:27:04 +09:00
leedj312 aea6924015 Merge commit '4be7354b36082993c69f3fd4690cb94eb1c3ed29' into iipbs/wr/dev 2025-07-26 19:26:04 +09:00
jyha00 4be7354b36 시설명 input 변경 2025-07-26 19:24:38 +09:00
mind-one005 ab23e4ba2c 하수관로 자체저장정보 .. 2025-07-26 18:55:14 +09:00
leedj312 e688a703ca Merge commit '49961db9a9904476a396f6efdfcbb092f7f111cf' into iipbs/wr/dev 2025-07-26 18:17:04 +09:00
jyha00 49961db9a9 차트별 조회된 데이터 없을 시 안내문구 추가 2025-07-26 18:16:46 +09:00
leedj312 56f8d3700a Merge commit 'a09e7fc709995b7ce29058d45dec4a614671d145' into iipbs/wr/dev 2025-07-26 17:32:14 +09:00
leedj312 c68e1d017e dm 현황 수정 및 env 수정 2025-07-26 17:31:32 +09:00
jyha00 a09e7fc709 재이용실적 현황 테이블 데이터 오류 수정 2025-07-26 17:20:54 +09:00
leedj312 024f25f003 수요처 검토 및 env 수정 2025-07-26 17:01:44 +09:00
mind-one002 9d101666ab main 머지 이슈로 수정. 2025-07-26 16:46:19 +09:00
mind-one002 f5ae14de9e Merge remote-tracking branch 'origin/iipbs/wr/prfSttn' into iipbs/wr/dev 2025-07-26 16:42:59 +09:00
jcchoi 4917084bbf Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-07-26 16:07:26 +09:00
jcchoi 5c7a8df55f currentPath 추가 2025-07-26 16:07:19 +09:00
yschoi 8656f6fb10 dm 추가 2025-07-26 16:04:47 +09:00
yschoi 4f4d823c2a Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-07-26 16:00:31 +09:00
yschoi 1d77247bcd Merge remote-tracking branch 'origin/iipbs/dm/dev' 2025-07-26 15:59:51 +09:00
jyha00 fe6293eb28 Merge branch 'iipbs/common/main' into iipbs/wr/prfSttn 2025-07-26 15:59:08 +09:00
jyha00 662fb390bc 재이용실적 현황 데이터 조회 수정 2025-07-26 15:55:37 +09:00
yschoi c037be3456 Merge remote-tracking branch 'origin/iipbs/sn/dev' 2025-07-26 15:55:31 +09:00
yschoi 4b54b798a3 Merge remote-tracking branch 'origin/iipbs/fo/dev' 2025-07-26 15:53:15 +09:00
leedj312 63cfc16ba5 대민 변경사항 수정 사항 반영완료 2025-07-26 15:39:07 +09:00
yschoi b28edb5a83 지도 스타일 수정 2025-07-26 15:36:02 +09:00
yschoi 719052d777 Merge remote-tracking branch 'origin/main' into iipbs/sn/dev 2025-07-26 15:34:39 +09:00
mind-one002 22d71c30f9 Merge branch 'iipbs/wr/dev' 2025-07-26 15:15:01 +09:00
mind-one005 562a562d15 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-07-26 15:14:03 +09:00
mind-one005 f94cb832a0 Merge branch 'iipbs/common/main' into iipbs/sn/dev 2025-07-26 15:04:05 +09:00
leedj312 87be84f620 실적등록 수정 2025-07-26 13:23:11 +09:00
leedj312 7661823c8b 스토어 아이디 env 프로덕션 수정 2025-07-26 10:30:00 +09:00
rsone001 2aba7bb16e ESlint 문법 에러 수정 2025-07-25 22:49:06 +09:00
mind-one005 735304cee0 Merge branch 'iipbs/sn/dev' 2025-07-25 20:43:15 +09:00
mind-one005 1ca2900a63 조회click시 paging초기화 2025-07-25 20:41:38 +09:00
100JM 53c966fc11 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-07-25 20:09:05 +09:00
Your Name 9da2e0054c 대시보드 수정 - 지하수유입량파트 2025-07-25 19:55:54 +09:00
chanii8 03efe1dfb5 Merge branch 'iipbs_cs' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-07-25 19:52:17 +09:00
chanii8 4ee840fa82 chore: pnpm-lock 업데이트 2025-07-25 18:56:54 +09:00
swcho 64a8ebbdbb Merge branch 'iipbs/sn/ii' into iipbs/sn/dev 2025-07-25 18:50:37 +09:00
swcho e8da4ac531 fix: 하수관망도, I/I 분석, 운영정보 페이지 페이지네이션 에러 수정 2025-07-25 18:50:12 +09:00
leedj312 bf77775cd6 Merge commit '711adc2e170ddc84dc81638ff6b85e8aa416df43' into iipbs/wr/dev 2025-07-25 18:47:15 +09:00
chanii8 9a62ef6276 [apps/cs] chore: 숫자에 toLocaleString() 메서드 적용 2025-07-25 18:44:30 +09:00
JIeunhuh 552ae18feb Merge commit '4920b4c1a26d81c9078e22c49bff1adda3eb834f' into iipbs/wr/dev 2025-07-25 18:39:31 +09:00
chanii8 bd999eb0f3 [apps/cs] chore: 수정 요청 사항 반영 2025-07-25 18:38:56 +09:00
JIeunhuh bd0c22c129 Merge commit 'd389390b0c25db4a9b229a2d76e99ff922d951c6' into iipbs/wr/dev 2025-07-25 18:37:36 +09:00
Your Name 83cb4ec7fa 빌드에러 수정 2025-07-25 18:34:03 +09:00
swcho ed4279d321 fix: SearchModal 페이지네이션 초기화 2025-07-25 18:31:09 +09:00
Your Name c43b5521ae 대시보드 계획정보 최신날짜의 계획정보 불러오도록 수정 2025-07-25 18:30:37 +09:00
JIeunhuh d389390b0c 수요처 상세, 리스트 현황 구현 완료 2025-07-25 18:30:14 +09:00
JIeunhuh 60ae8c9e72 수요처 작업중 2025-07-25 18:08:28 +09:00
mind-one003 b543ff6334 build 오류 해결. 2025-07-25 05:06:47 -04:00
mind-one003 f744957447 지하수 유입량 모달 부분 추가. 2025-07-25 05:03:08 -04:00
swcho e3de7ec112 Merge branch 'iipbs/sn/ii' into iipbs/sn/dev 2025-07-25 18:02:18 +09:00
swcho 61ed66d4f0 feat: SearchModal 데이터 포매팅 2025-07-25 18:00:05 +09:00
100JM 393e174a6d 연계 모듈 관리 2025-07-25 17:39:13 +09:00
mind-one005 294da35347 오류 표시 수정 2025-07-25 16:32:13 +09:00
yschoi 44103a8dc4 미니맵 지도 기능 제거 2025-07-25 16:15:53 +09:00
Your Name ea36df1c29 조회버튼 기능완료, 초기화 시 수영으로 초기화 완료 2025-07-25 15:49:06 +09:00
leedj312 814dac672c Merge commit '5ce1c571ed75c09675361421ac9947412ac8e57c' into iipbs/dm/dev 2025-07-25 15:47:11 +09:00
leedj312 88838dda0b 수요처 저장 함수 추가 2025-07-25 15:46:34 +09:00
yschoi 5ce1c571ed 후보지 추가 지도 진행시켜~~~ 2 2025-07-25 15:43:57 +09:00
yschoi f2881f3a3a 후보지 추가 지도 진행시켜~~~ 2025-07-25 15:29:20 +09:00
chanii8 78eb2d8fcd [apps/cs] chore: 이미지 등록 폼 수정 2025-07-25 15:11:41 +09:00
swcho 89ca295b69 fix:
- 침입수, RDII 분석결과 페이지 페이징 처리, 삭제 후 조회 로직 에러 수정
2025-07-25 15:08:39 +09:00
mind-one005 9a3378d07b 반올림 & , 적용 2025-07-25 15:02:04 +09:00
leedj312 c0eb6b94cc 가몸관련 수정 완료 2025-07-25 14:46:28 +09:00
Your Name 12f81754a0 Merge branch 'iipbs/common/main' into iipbs/fo/dev 2025-07-25 14:31:45 +09:00
chanii8 d7233992fe [apps/cs] feat: 접속자 통계 페이지 레이아웃 작업 2025-07-25 14:30:33 +09:00
leedj312 c29d191a5e 계산식에서 *365 빼기 2025-07-25 14:27:15 +09:00
swcho b980321557 fix:
- 운영정보 검색 한 페이지에 4개씩으로 변경 (화면 비율 고려)
- 상세페이지 X 버튼 위치 고정
2025-07-25 14:24:24 +09:00
leedj312 3a37192782 Merge remote-tracking branch 'origin/iipbs/wr/login-test' into iipbs/wr/dev 2025-07-25 14:07:37 +09:00
leedj312 f3ca55d183 빌드 수정 2025-07-25 14:07:25 +09:00
yschoi 6955164f71 Merge branch 'iipbs/dm/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/dm/dev 2025-07-25 14:07:09 +09:00
yschoi d20bfb1bf1 후보지 선택 지도 작업중 2025-07-25 14:07:00 +09:00
Your Name fa8db521d5 Merge branch 'iipbs/common/main' into iipbs/fo/dev 2025-07-25 14:04:36 +09:00
leedj312 506eb78a80 Merge commit '32b16055fb34e758084b993b8e786140048f5645' into iipbs/wr/dev 2025-07-25 13:45:47 +09:00
leedj312 ce0ba4ff40 버튼 추가 2025-07-25 13:40:25 +09:00
JIeunhuh 42795e517b x축 스크롤 제거 2025-07-25 13:06:19 +09:00
swcho 590adb5b4e fix: 오수펌프장, 우수펌프장 명칭 바뀐 것 수정 2025-07-25 11:42:51 +09:00
mind-one005 c204d90d14 rdii 그래프 수정 2025-07-25 11:27:57 +09:00
JIeunhuh e9bfaa865a 지도 팝업 사이즈 조정 및 상세클릭시에 화면 가운데로 오도록 조정 2025-07-25 11:12:07 +09:00
mind-one005 e0bc5869d6 Merge branch 'iipbs/common/main' into iipbs/sn/dev 2025-07-25 10:54:00 +09:00
mind-one005 b8da4d7d42 Merge branch 'iipbs/common/main' into iipbs/sn/dev 2025-07-25 10:45:01 +09:00
leedj312 70f250f7bb Merge commit 'd465719c6f53de5c5b27ccfe09f643db502f7f7b' into iipbs/dm/dev 2025-07-25 10:42:58 +09:00
jyha00 d465719c6f 대민 화면 수정 2025-07-25 10:40:08 +09:00
leedj312 bc3049c255 url 수정 2025-07-25 10:22:47 +09:00
Your Name 8624b36e89 Merge branch 'iipbs/common/main' into iipbs/fo/dev 2025-07-25 10:14:39 +09:00
leedj312 e4b1c78102 Merge commit '6fcc97caf18a9ce628e18503eb89ece739dc8890' into iipbs/dm/dev 2025-07-25 10:06:28 +09:00
swcho be47ac0edb fix:
- 하수처리시설 정보에서 디지털 자료관 연계 정보 기능 주석 처리
- 운영정보 검색 시 페이지 리셋 추가
- 하수관로 연계정보에서 사업명 줄바꿈 처리
2025-07-25 10:06:19 +09:00
leedj312 6fcc97caf1 초기화면 으로 이동 수정 토큰 적용 2025-07-25 10:04:58 +09:00
yschoi 6bdef28532 후보지 조회 지도 팝업 생성 2025-07-25 10:03:28 +09:00
Your Name 314e6ed9ad 조치진행중
- 강우량 숨김
- 수질조회 2023년으로 세팅
2025-07-25 09:57:47 +09:00
Your Name 99ffa1d6d4 대시보드 2021년 부산 수영으로 default 세팅 2025-07-25 09:38:50 +09:00
mind-one002 d23c9ca4b1 2023년도 기준으로 재 수정. 2025-07-25 04:53:53 +09:00
mind-one002 cc67d707d9 default 값이 2023으로 우선 고정 처리. 2025-07-25 04:49:18 +09:00
mind-one002 583559ec60 오수발생량에 기준년도 2023으로 수정. 2025-07-25 04:35:24 +09:00
mind-one002 e7195f7db0 오수발생량 나오도록 수정. 2025-07-25 04:26:17 +09:00
mind-one002 04bb3657bb idx 불필요로 삭제. 2025-07-25 04:16:27 +09:00
mind-one002 f1563aa1fc 하수사용량 수치 오류 부분 수정. 2025-07-25 04:03:03 +09:00
swcho 0795299b48 feat: 상류관로 보기 기능에 로딩바 추가 2025-07-25 00:58:55 +09:00
swcho 3380addd78 Merge branch 'iipbs/common/main' into iipbs/sn/ii 2025-07-25 00:50:40 +09:00
swcho c9e413a3d3 Merge branch 'iipbs/sn/dev' into iipbs/sn/ii 2025-07-25 00:28:27 +09:00
swcho 6413bc6b18 feat: featureClickEventPopup 하수맨홀 선택 시 상류관로 보기 기능 구현 2025-07-25 00:28:01 +09:00
swcho fbb47c2593 Merge branch 'iipbs/common/main' into iipbs/sn/ii 2025-07-25 00:25:24 +09:00
truthwing ec7655a71d Merge commit 'c8d2c4e17ed3b3f42ac32860f8851e5f1e472387' into iipbs/fo/dev 2025-07-24 23:38:48 +09:00
jsyang c8d2c4e17e 조치 진행 중, pnpm build any제외 작업진행 2025-07-24 10:17:58 -04:00
mind-one005 b513fddbd5 LineBarchart 1차 2025-07-24 22:13:37 +09:00
mind-one005 c0c7bdfa5b Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2025-07-24 22:06:34 +09:00
mind-one005 55b8e91298 Merge branch 'iipbs/common/main' into iipbs/sn/dev 2025-07-24 22:04:25 +09:00
Your Name cd08b5dad8 조치 진행중 2025-07-24 21:58:50 +09:00
yschoi 3a73c8bf1c lock 삭제 2025-07-24 21:21:10 +09:00
yschoi 4389a6ddb7 Merge remote-tracking branch 'origin/main' into iipbs/dm/dev 2025-07-24 21:20:28 +09:00
yschoi 8a2ed7df58 하수처리시설 팝업 사이즈 조정 2025-07-24 21:16:26 +09:00
yschoi 00d6ff90c6 Merge remote-tracking branch 'origin/main' into iipbs/wr/dev 2025-07-24 20:56:28 +09:00
yschoi 9bfd32b5c2 Merge remote-tracking branch 'origin/main' into iipbs/sn/dev 2025-07-24 20:51:46 +09:00
yschoi f57dcc9788 Merge branch 'iipbs/dm/map' into iipbs/dm/dev 2025-07-24 20:50:38 +09:00
yschoi f7849f317c Merge branch 'iipbs/dm/map' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/dm/map 2025-07-24 20:47:42 +09:00
yschoi 7cd1ae2625 gis 이미지 추가 2025-07-24 20:47:35 +09:00
swcho 159fad40ef Merge branch 'iipbs/sn/ii' into iipbs/sn/dev 2025-07-24 20:39:04 +09:00
swcho 4aa4089ddd fix: featureClickEventPopup.tsx 클릭 이벤트 신에 관리번호 안 나오는 에러 수정 2025-07-24 20:38:28 +09:00
swcho f6cb75ff1c Merge remote-tracking branch 'origin/iipbs/sn/ii' into iipbs/sn/dev 2025-07-24 20:06:18 +09:00
swcho 61e4643503 Merge remote-tracking branch 'origin/iipbs/common/main' into iipbs/sn/ii 2025-07-24 20:01:59 +09:00
swcho 8ed0a86cf5 fix: SearchModal 검색 로직 에러 수정 (상세 정보 표시, zoom 이동은 아직) 2025-07-24 19:58:20 +09:00
leedj312 52170fc12f Merge commit '4c8ecb05e094965df93dcf474bffa79c5135e61e' into iipbs/dm/dev 2025-07-24 19:57:10 +09:00
leedj312 4c8ecb05e0 서치바 컴포넌트 추가 완료 2025-07-24 19:51:48 +09:00
jyha00 9e95ad3fad Merge branch 'iipbs/common/main' into iipbs/dm/design 2025-07-24 19:41:31 +09:00
yschoi a11f760162 gis 기능 수정 2025-07-24 19:08:14 +09:00
chanii8 e35b73d067 Merge branch 'iipbs_cs' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-07-24 18:16:00 +09:00
chanii8 2620e40872 [common/components] feat: Header, LNB 컴포넌트 비로그인 상태 분기처리 2025-07-24 17:57:46 +09:00
jyha00 79bc10cdce 대민 디자인 수정 2025-07-24 17:37:46 +09:00
Your Name 5a43b33a1d 강우량 및 단위 수정 2025-07-24 15:08:07 +09:00
leedj312 c9f1d05126 서치바 컴포넌트 일단 추가 2025-07-24 15:01:29 +09:00
leedj312 19bee65195 메인페이지에서 지도팝업(버튼) 추가 2025-07-24 15:01:08 +09:00
leedj312 26c01ede4e 대민 지도 팝업 페이지 생성 2025-07-24 14:51:04 +09:00
leedj312 c5f4f6ddd0 물재이용 화면 건축물 재이용여부 수정 2025-07-24 14:09:43 +09:00
leedj312 f332d64aa6 물재이용 화면별 등록/수정 권한 추가 2025-07-24 14:06:20 +09:00
yschoi bb4d7b9d56 물재이용시설 팝업 2025-07-24 13:38:40 +09:00
swcho f6cefd6230 fix: API 조회 시 빈 데이터일 경우 에러 처리 2025-07-24 13:22:21 +09:00
swcho c6b45499c4 fix: SearchModal 컴포넌트 내의 SelectBox들 z-index 수정 2025-07-24 13:11:45 +09:00
swcho 30bbf51e77 refactor: basicTab 구분 별로 UI 컴포넌트 분리 2025-07-24 13:01:11 +09:00
yschoi bb6dbfa617 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-07-24 11:13:44 +09:00
swcho 1778c07ef9 refactor: searchModal '하수도정비기본계획보고서' 탭 UI 컴포넌트 분리 2025-07-24 11:12:12 +09:00
yschoi 075e3711a9 운영 설정 추가 2025-07-24 11:08:46 +09:00
jyha00 32b16055fb 하수재이용시설 현황 모달 오류 수정 및 필요없는 주석 삭제 2025-07-24 11:05:19 +09:00
jyha00 1daa925019 잠재수요처 권한 추가 2025-07-24 11:04:41 +09:00
chanii8 f58f9c3fe3 Merge branch 'iipbs_cs' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-07-24 10:54:18 +09:00
chanii8 af98524806 [apps/cs] feat: 대시보드 페이지 작업 2025-07-24 10:53:48 +09:00
chanii8 ecf430dd33 Merge branch 'iipbs_cs' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-07-24 10:03:32 +09:00
chanii8 892e329207 chore: lock 파일 업데이트 2025-07-24 10:03:24 +09:00
leedj312 543fbb70cc 로그인 아이디 변경 2025-07-24 09:33:52 +09:00
swcho 3a191e8d06 refactor: searchModal '운영정보' 탭 UI 컴포넌트 분리 2025-07-24 09:10:30 +09:00
swcho 402b9a5539 refactor: searchModal '기본정보' 탭 UI 컴포넌트 분리 2025-07-23 19:33:19 +09:00
swcho ef80915519 refactor: SearchModal 한글 변수명 수정 2025-07-23 19:11:17 +09:00
swcho 2e83898777 feat: SearchModal 한글 변수명 수정 2025-07-23 18:58:16 +09:00
swcho 3d988b4644 feat: SelectBox2 컴포넌트 -> SelectBox 컴포넌트로 변경 2025-07-23 18:13:11 +09:00
jyha00 bcdefaa828 재이용실적 등록, 지도점검 등록 권한 설정 추가 2025-07-23 17:58:07 +09:00
leedj312 0d48a0b579 페이지별 이벤트 권한체크, 시설 등록,현황, 수요처 관리 까지 완료 2025-07-23 17:51:47 +09:00
swcho c59c17f9f0 Merge branch 'iipbs/common/main' into iipbs/sn/ii 2025-07-23 17:45:54 +09:00
chanii8 55793d8463 [common/components] refactor: 메뉴 활성화 조건 변경(menuCd) 2025-07-23 16:46:47 +09:00
Your Name c4750d9652 수요처명 입력오류 수정 2025-07-23 13:48:38 +09:00
leedj312 fc3ef2c59e Merge branch 'iipbs/wr/dev' into iipbs/wr/login-test 2025-07-23 13:16:03 +09:00
jyha00 accc3e0ef1 빗물 엑셀 다운로드 데이터 오류 수정 2025-07-23 13:13:33 +09:00
swcho ba657e8311 Merge branch 'iipbs/sn/dev' 2025-07-23 12:15:19 +09:00
swcho 53fef3bdbf fix: 운영정보 조회 사업명 Input 컴포넌트 숫자만 입력되는 에러 수정 2025-07-23 12:15:06 +09:00
swcho 21afb058cf Merge branch 'iipbs/sn/dev' 2025-07-23 12:10:56 +09:00
swcho 5232703dd9 fix: fetchFilteredData bizTyCd 조건문 수정 2025-07-23 12:08:00 +09:00
swcho 406ce4056a feat, fix:
- 운영정보 업무구분으로 조회 기능 구현
- 용도, 재질 코드 -> 이름으로 수정
- DetailTable 페이지네이션 수정
2025-07-23 12:01:41 +09:00
truthwing 21e0ea086e Merge commit 'f50a9aaf3a41fb542c18f6f655ee5adeb69b4258' 2025-07-23 10:46:32 +09:00
YangJinSoo94 142929510f 포트변경 2025-07-23 10:16:11 +09:00
YangJinSoo94 f50a9aaf3a 임시로 개발서버로 바라보고 수정 2025-07-23 10:08:47 +09:00
YangJinSoo94 60e73212e8 hostm port 변경경 2025-07-23 09:19:15 +09:00
swcho 6660c85150 Merge remote-tracking branch 'origin/iipbs/sn/dev' 2025-07-22 22:24:16 +09:00
swcho 2cfd57fdc3 fix: 하수관로 상세페이지 useEffect 의존성 배열 수정 2025-07-22 22:23:00 +09:00
swcho 5e5411a34f Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2025-07-22 22:19:09 +09:00
swcho 01852c1528 refactor:
- MsSwagePpl.tsx 리팩토링
- index.ts Merge
2025-07-22 22:16:34 +09:00
mind-one005 4488bd23c7 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-07-22 21:49:36 +09:00
mind-one005 c2696c6eeb Merge branch 'iipbs/sn/dev' 2025-07-22 21:49:21 +09:00
mind-one005 4ef86a3e6a 기본계획 download 수정 2025-07-22 21:48:37 +09:00
truthwing badea83e9e Merge commit '268c45ed05efa45f966d33556a6ea974472b4b81' 2025-07-22 21:26:32 +09:00
truthwing fc2f119584 Merge commit '4e2fe7aaa1433c22886685d802662872df6e3c11' 2025-07-22 21:26:21 +09:00
truthwing 23a92ec38c Merge commit 'fe2f3faee4bc2250684b494c14c692aa45f08b28' 2025-07-22 21:25:58 +09:00
truthwing 268c45ed05 jsyang product 2025-07-22 21:24:43 +09:00
truthwing fe2f3faee4 js cookie 재설치 2025-07-22 21:15:33 +09:00
swcho 3e5a14fbc8 Merge branch 'iipbs/sn/ii' into iipbs/sn/dev 2025-07-22 21:08:17 +09:00
truthwing e891702da2 build 오류 해결 jscookies 문제 확인 필요 2025-07-22 21:06:50 +09:00
swcho 527b4293c4 feat, refactor:
- 하수관로 상세 페이지  운영정보 연계 기능 구현
- 운영정보 탭 검색 기능 구현
- UITab2, UITable2 삭제 후 공통 컴포넌트 적용 (UITab, UITable)
- SearchModal.tsx UI 수정
- featureClickEvent에 isClicked state 추가
2025-07-22 21:06:09 +09:00
leedj312 4e2fe7aaa1 Merge commit '0a109111cbf2cae77a348bddb2116a56950852e8' into iipbs/wr/dev 2025-07-22 20:58:02 +09:00
jyha00 0a109111cb 중수도통계 상세 엑셀 다운로드 수정 2025-07-22 20:55:14 +09:00
leedj312 3b6e8ceedb 빗물 통계 수정 2025-07-22 20:55:00 +09:00
jyha00 99229a8453 불필요한 출력창 삭제 2025-07-22 20:41:37 +09:00
jyha00 383e5f8382 중수도통계 테이블, 엑셀 수정 2025-07-22 20:38:42 +09:00
mind-one005 a11570b563 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-07-22 20:17:58 +09:00
mind-one005 af1feb3920 Merge branch 'iipbs/sn/dev' 2025-07-22 20:17:52 +09:00
mind-one005 a6df8d261a RSON 제거 2025-07-22 20:15:14 +09:00
truthwing f96942beee LOCAL_URL 경로 오류로 수정. 2025-07-22 20:08:32 +09:00
truthwing 7994698349 js-cookie 추가. 2025-07-22 19:48:04 +09:00
swcho ab495c6572 Merge remote-tracking branch 'origin/iipbs/common/main' into iipbs/sn/ii 2025-07-22 16:30:23 +09:00
swcho 47c986d517 Merge remote-tracking branch 'origin/iipbs/common/main' into iipbs/sn/ii 2025-07-22 16:17:45 +09:00
YangJinSoo94 425eb39a86 middleware 작업진행중 2025-07-22 16:01:48 +09:00
leedj312 e30a7d184b 권한 체크 작업중 작업해야함 2025-07-22 15:57:20 +09:00
YangJinSoo94 1ec1c42e9f middleware 작업진행중중 2025-07-22 15:43:54 +09:00
leedj312 b4e3d922b1 Merge commit 'a028b6ae8bba8e074e82900d05925d4e5a9c6951' into iipbs/wr/dev 2025-07-22 15:38:12 +09:00
swcho 9565931042 Merge remote-tracking branch 'origin/iipbs/common/main' into iipbs/sn/ii 2025-07-22 15:28:36 +09:00
yschoi 1042391702 Merge remote-tracking branch 'origin/iipbs/dm/dev' 2025-07-22 15:12:49 +09:00
yschoi 68f468fa2c output: "standalone", 추가 2025-07-22 15:12:06 +09:00
YangJinSoo94 fb9f2a9d4a middleware 작업진행중중 2025-07-22 15:03:24 +09:00
yschoi 7c2ff7c037 build": "turbo run build 2025-07-22 14:48:33 +09:00
leedj312 ad0803f405 Merge commit '03675aaa6062402fbb3772e97cd966c0f7abd890' into iipbs/wr/dev 2025-07-22 14:38:24 +09:00
jyha00 03675aaa60 재이용통계 엑셀 다운로드시 시도명 없음 수정 2025-07-22 14:36:09 +09:00
leedj312 c0c19c0425 매칭완료 오류사항 수정 2025-07-22 14:32:25 +09:00
yschoi 0bbb27e3d3 불필요 삭제 2025-07-22 13:58:24 +09:00
leedj312 214d283366 Merge commit '7d95ddf6a521f108b3b49d71760ba6e92a56d2dc' into iipbs/wr/login-test 2025-07-22 13:56:46 +09:00
jyha00 a19c5c269a 지도점검 테이블 재수정 2025-07-22 13:37:35 +09:00
YangJinSoo94 d641866cc4 -대시보드 메뉴 및 로그인 기능완료, 권한 완료
-대시보드 운영현황 데이터 수정, 시설명 div z-index 수정정
2025-07-22 13:21:39 +09:00
jyha00 6102e0af6b 지도점검 테이블 수정 2025-07-22 13:10:22 +09:00
jyha00 225bae08d3 readOnly 수정 2025-07-22 13:09:20 +09:00
leedj312 d8b5abb12f 미들웨어 반영 기본값 반영 2025-07-22 10:36:56 +09:00
truthwing e4de937f6e Merge commit 'baabc72f5840bc882c1d7178129707a9ca1ea2ae' 2025-07-22 10:15:36 +09:00
chanii8 ba6d7f4177 [apps/cs] chore: 메뉴 삭제 시 폼 초기화 2025-07-22 10:13:53 +09:00
leedj312 d9a3da96bb 베이스 변경사항 2025-07-22 09:24:19 +09:00
leedj312 87d1f33c05 Merge commit 'd68c6c151c5135ccb1f326fd36cd4b02200106f7' into iipbs/wr/login-test 2025-07-22 09:23:56 +09:00
chanii8 56e9218646 [apps/cs] refactor: 기간 선택 컴포넌트 수정 2025-07-21 21:46:11 +09:00
chanii8 2d5534b3a6 [common/components] feat: InputDatePicker 컴포넌트 추가 2025-07-21 21:45:34 +09:00
mind-one005 05331db008 Merge branch 'iipbs/sn/dev' 2025-07-21 21:21:27 +09:00
mind-one005 f0cb735d69 그래프 명 수정 2025-07-21 21:05:44 +09:00
leedj312 d68c6c151c Merge commit 'ddf3a5cfa0aedbb2bb73e073ff97fa68ca2c5d9a' into iipbs/wr/dev 2025-07-21 20:00:43 +09:00
leedj312 dfb910f270 Merge commit '5976b35c902b1fb1da1fdc637e620a0d0d3e9ede' into iipbs/wr/dev 2025-07-21 19:58:48 +09:00
jyha00 5976b35c90 결함 오류 수정 2025-07-21 19:56:41 +09:00
truthwing 6b204fce15 djlee product 2025-07-21 19:51:52 +09:00
leedj312 f731d0f9f0 변경사항 2025-07-21 19:04:13 +09:00
leedj312 e231d0c4be 수정 사항 발견 2025-07-21 18:05:13 +09:00
leedj312 e4328a2ed4 Merge commit '069e489d031203200fc4d73e470767503a3dc227' into iipbs/wr/dev 2025-07-21 18:01:43 +09:00
leedj312 069e489d03 온배수 현황 수질 작업중 2025-07-21 17:59:55 +09:00
leedj312 990dffc11c 물재이용 / 이동시 메인으로 이동 2025-07-21 17:59:38 +09:00
jyha00 b4140d6457 결함 수정 2025-07-21 17:50:47 +09:00
jyha00 91af5fd27c Merge branch 'iipbs/common/main' into iipbs/wr/pfmncSttn 2025-07-21 16:15:56 +09:00
yschoi 588e4984b3 잘못 머지 된듯 2025-07-21 15:34:08 +09:00
YangJinSoo94 baabc72f58 빌드에러수정 2025-07-21 15:29:08 +09:00
jyha00 be7efb2ad0 결함 오류 수정 2025-07-21 15:27:18 +09:00
yschoi 1dab522bd2 운영 env 추가 2025-07-21 15:17:15 +09:00
YangJinSoo94 7a24c10f3d 이미지 수정, 빌드 오류 수정 2025-07-21 14:44:01 +09:00
YangJinSoo94 7525fa01b6 dm merge 2025-07-21 13:44:28 +09:00
jyha00 fbd9575df7 잠재수요처 페이지 오류 수정 2025-07-21 11:07:51 +09:00
truthwing 14608cca4d Merge remote-tracking branch 'origin/iipbs/wr/dev' 2025-07-21 10:11:39 +09:00
truthwing 0dab113a3f Merge commit '65d2cbd73284d64bddc0c5495a97cf094e518f6a' 2025-07-21 10:07:02 +09:00
chanii8 6d967497ba refactor: 게시 기간 선택 로직 수정 2025-07-20 23:15:36 +09:00
yschoi 95fc7a22dc 운영 env 수정 2025-07-19 22:15:18 +09:00
yschoi 9c69ba2122 output: "standalone", 2025-07-19 19:01:24 +09:00
yschoi 978a049277 개발서버 설정중 2025-07-19 18:49:09 +09:00
yschoi 538065e29e 개발 배포 설정 2025-07-19 18:40:37 +09:00
yschoi 99a8f874e8 test 2025-07-19 18:16:56 +09:00
mind-one005 71dd649624 developement 환경변수 자료관 주소 추가 2025-07-19 15:24:50 +09:00
mind-one005 63a0cc70af development 환경변수 추가 2025-07-19 15:22:33 +09:00
yschoi e70dec580c Merge remote-tracking branch 'origin/main' into iipbs/sn/dev 2025-07-19 14:43:13 +09:00
mind-one005 981d700624 II chart 닫기버튼 추가 2025-07-19 13:41:38 +09:00
mind-one005 9ff9e832ea auth cookie 2025-07-19 12:07:15 +09:00
chanii8 b933b9149e Merge branch 'iipbs_cs' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-07-18 19:00:00 +09:00
chanii8 58c435d67e [apps/cs] chore: 트리메뉴에 툴팁 적용 2025-07-18 18:59:45 +09:00
chanii8 950355717c [apps/cs] feat: 권한맵핑 다이얼로그 내 검색 기능 추가 2025-07-18 18:47:24 +09:00
jyha00 4c955beff2 결함 수정 2025-07-18 18:11:09 +09:00
chanii8 823ed50f37 Revert "[common/components] chore: LNB 메뉴 활성화 조건 수정"
This reverts commit d7d2e2d3ef.
2025-07-18 17:21:11 +09:00
chanii8 4d9b6235b3 [apps/cs] feat: 권한관리 등록 시 관리 사이트 추가 2025-07-18 17:21:06 +09:00
chanii8 500e966ec7 Merge branch 'iipbs_cs' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-07-18 11:05:46 +09:00
chanii8 d7d2e2d3ef [common/components] chore: LNB 메뉴 활성화 조건 수정 2025-07-18 09:30:22 +09:00
chanii8 617eb217e1 [apps/cs] feat: 사이트맵 관리 기능 추가 2025-07-18 00:25:16 +09:00
leedj312 ae41ea100c map 주소 변경 2025-07-17 19:53:16 +09:00
leedj312 07e34beca1 Merge commit 'ae97a48ba246ef429633640b97742adbb524c239' into iipbs/wr/dev 2025-07-17 19:33:01 +09:00
leedj312 87d8ddef98 Merge commit '8b29d0288005b94e2c9206d7f40c3975efb4a485' into iipbs/wr/dev 2025-07-17 19:31:42 +09:00
jyha00 8b29d02880 하수재이용/빗물/중수도/온배수 등록 페이지 전체적인 레이아웃 수정 2025-07-17 18:08:53 +09:00
yschoi db001315b7 Merge remote-tracking branch 'origin/main' into iipbs/sn/dev 2025-07-17 17:55:06 +09:00
yschoi 1e6796eeb9 지도 라이브러리 추가 2025-07-17 17:53:03 +09:00
swcho 7816b4e450 Merge remote-tracking branch 'origin/main' into iipbs/sn/ii 2025-07-17 17:27:17 +09:00
leedj312 ae97a48ba2 사이드바 매뉴이동 수정 2025-07-17 17:25:05 +09:00
swcho 240c2f847c Merge remote-tracking branch 'origin/iipbs_cs' into iipbs/sn/ii 2025-07-17 17:10:34 +09:00
swcho e39a102c14 Merge remote-tracking branch 'origin/iipbs/common/main' into iipbs/sn/ii 2025-07-17 17:08:36 +09:00
mind-one003 65d2cbd732 빌드에러 수정 2025-07-17 16:56:41 +09:00
leedj312 db70ba56c6 개발서버 env변경 2025-07-17 16:26:47 +09:00
leedj312 3fa1c526fe env 변경 2025-07-17 16:20:13 +09:00
chanii8 f15dd02cc9 [apps/cs] refactor: 트리 메뉴 엔티티 생성 2025-07-17 16:19:36 +09:00
leedj312 992893510e Merge remote-tracking branch 'origin/iipbs/wr/pfmncSttn' into iipbs/wr/dev 2025-07-17 16:05:11 +09:00
leedj312 5fe4bc94da Merge commit 'c22e07bc16afbc2888806b4e0d36606b8ea0d35f' into iipbs/wr/dev 2025-07-17 16:00:31 +09:00
leedj312 5f302a5711 로그인 테스트 작업중 2025-07-17 15:57:20 +09:00
chanii8 e337854904 [apps/cs] chore: steiger lint 실행 2025-07-17 15:39:34 +09:00
swcho 6a0572610b Merge branch 'iipbs/sn/ii' into iipbs/sn/dev 2025-07-17 14:43:41 +09:00
swcho 2850b1413f feat, fix:
- 하수처리 시설 검색 조건에 처리구역 면적 추가
- 우수토실, 하수처리시설, 하수처리구역 관리 번호 오류 수정
2025-07-17 14:23:37 +09:00
jyha00 cc749c0c24 Merge remote-tracking branch 'origin/iipbs/common/main' into iipbs/wr/pfmncSttn 2025-07-17 13:16:56 +09:00
yschoi e9ccb6bcac Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2025-07-17 10:21:30 +09:00
yschoi 0e516c647d 불필요 내용 삭제 2025-07-17 10:21:18 +09:00
swcho 98d11090f8 fix: 빌드 에러 수정 2025-07-16 21:20:03 +09:00
chanii8 0a5b3cbd31 Merge branch 'iipbs_cs' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-07-16 19:51:43 +09:00
chanii8 d0d713a273 [apps/cs, common/features] fix: 사이트 게시 팝업 '상시' 선택 시 기간 미설정 (CS_UT02020105) 2025-07-16 19:50:14 +09:00
swcho 20b45a02ed feat:
- 침입수 분석, RDII 분석 분석자 정보 추가
- SearchModal 하수맨홀 용도로 조회 기능 추가
- SearchModal 하수관로 조회 되게끔 수정
- table.ts에서 미분류 중복값 삭제
2025-07-16 19:49:29 +09:00
swcho 4c005c7b9e feat:
- 침입수 분석, RDII 분석 분석자 정보 추가
- SearchModal 하수맨홀 용도로 조회 기능 추가
- SearchModal  하수관로 조회 되게끔 수정
- table.ts에서 미분류 중복값 삭제
2025-07-16 19:48:54 +09:00
leedj312 bd6dfd31fd 엑셀 중수도 지도점검 데이터 확인 2025-07-16 19:27:24 +09:00
YangJinSoo94 e4f4f70bc9 유량현황, 수질현황 모달 엑셀다운로드 완료 2025-07-16 17:13:19 +09:00
jyha00 5aaa55d0c9 수요매칭검토 기본사항 readonly 추가 2025-07-16 16:29:25 +09:00
swcho 92356fd451 fix: package json build 의존성 선언 정비 (import하는 파일이 속한 패키지의 dependencies에 라이브러리 추가) 2025-07-16 15:49:53 +09:00
jyha00 5a3ef19e5d 하수재이용실적 현황 검색조건 수정 2025-07-16 15:32:18 +09:00
jyha00 01cb48e5b6 하수재이용시설 등록 보증수질 input 변경, 신규등록 저장 버튼 위치 수정, 상단 고정 추가 2025-07-16 14:55:44 +09:00
leedj312 ea7536bea3 안쓰는 함수 제거 2025-07-16 13:58:19 +09:00
leedj312 2356c10d34 빗물지도점검 오류 검증 2025-07-16 13:43:03 +09:00
jyha00 8b2dd27a79 하수재이용시설 등록 저장 버튼 위치 변경 2025-07-16 13:37:57 +09:00
YangJinSoo94 114293bb00 Merge branch 'iipbs/common/main' into iipbs/fo/dev 2025-07-16 13:32:44 +09:00
chanii8 dd5af43d33 [apps/cs] chore: 그룹관리 내 사용자 추가 팝업 수정 (CS_UT05020103) 2025-07-16 13:30:41 +09:00
YangJinSoo94 d7db174c1d 대시보드 차트 색상 수정, 모달 강우량 컬럼추가 2025-07-16 13:29:45 +09:00
jyha00 079542cf74 Merge branch 'iipbs/common/main' into iipbs/wr/pfmncSttn 2025-07-16 13:20:32 +09:00
chanii8 0a3a84bb98 [apps/cs] feat: 회원 상태 뱃지로 표시 (CS_UT05010109) 2025-07-16 12:08:18 +09:00
swcho a403113bbc feat:
- '자체 입력정보' -> '입력 정보'로 수정
- 용도 코드, 재질 코드 텍스트로 매핑 (일단 하드코딩)
2025-07-16 11:37:13 +09:00
chanii8 e709d3cca3 chore: pnpm-lock 파일 업데이트 2025-07-16 11:21:30 +09:00
swcho 8de9760099 feat: 침입수 분석, RDII 분석 Request Body에 UUID 추가 2025-07-16 11:12:00 +09:00
yschoi 5015b8e0d9 output: "standalone", 추가 2025-07-16 11:11:50 +09:00
swcho 8f3db94069 refactor: UIModal 사용 페이지 초기화 로직 변경 2025-07-16 11:01:05 +09:00
YangJinSoo94 4d0c0ad48c 오류수정 2025-07-16 10:19:03 +09:00
swcho 9af9bfcae8 Merge branch 'iipbs/common/main' into iipbs/sn/ii 2025-07-16 10:04:51 +09:00
chanii8 fad4a14a82 [apps/cs] fix: 콘텐츠 등록 폼 버그 수정 (CS_UT03020102) 2025-07-16 09:53:24 +09:00
chanii8 21aab85ff8 [apps/cs] feat: 미들웨어 로깅 추가 2025-07-15 23:01:56 +09:00
truthwing 7db61928b1 양진수 대리 컴퓨터 바꿔야하는 오류 발견. 2025-07-15 21:03:56 +09:00
YangJinSoo94 3dae039765 first init 2025-07-15 20:54:38 +09:00
leedj312 35ff13fb38 헤드리스트 수정 중 2025-07-15 20:27:43 +09:00
leedj312 231c0c2940 Merge commit 'a92f567e5c624486d80984c5e4427543aafdfdde' into iipbs/wr/dev 2025-07-15 20:27:30 +09:00
jyha00 a92f567e5c 재이용실적 현황 페이지 오류 수정 2025-07-15 20:23:02 +09:00
leedj312 ff9d373fcd Merge commit 'c111c2c084cfe533453f568d17c9fa5bcba02797' into iipbs/wr/dev 2025-07-15 20:22:57 +09:00
leedj312 c111c2c084 서버설정 작업 완료 2025-07-15 20:22:31 +09:00
yschoi 4023423220 output: "standalone", wr 2025-07-15 17:18:10 +09:00
leedj312 2d5a08a271 안쓰는 코드 삭제 2025-07-15 17:16:37 +09:00
yschoi d20b526ff1 빌드 standalone 추가 2025-07-15 17:16:19 +09:00
leedj312 9a8abee77c 빌드에러 수정 2025-07-15 16:52:42 +09:00
jyha00 4c30c2fa47 물재이용 페이지별 오류 수정 2025-07-15 15:40:55 +09:00
leedj312 0cca1c8dbe 빌드 확인필요 2025-07-15 15:39:32 +09:00
swcho ae0384af83 feat: 상세정보 페이지 중분류 표시, 관리번호 표시 기능 추가 2025-07-15 15:38:42 +09:00
chanii8 9e5c30be8a [apps/cs] feat: 권한관리 목록 내 삭제 기능 추가 (CS_UT04010104) 2025-07-15 15:21:07 +09:00
swcho 58a2b6c5c0 Merge branch 'iipbs/common/main' into iipbs/sn/ii 2025-07-15 14:11:54 +09:00
jyha00 def7b2b01c 지도점검 등록 및 현황 단위 추가, 용수명 가운데 정렬로 수정 2025-07-15 13:54:30 +09:00
chanii8 e57d716559 [apps/cs] chore: 관리자관리 > 관리자 목록 조회 기본 파라미터 수정 (CS_UT04010101) 2025-07-15 13:50:47 +09:00
chanii8 586efe940f [apps/cs] feat: 조직도관리 > 사용자추가 로직 수정 (CS_UT03040105) 2025-07-15 13:38:36 +09:00
chanii8 61f261a25b [apps/cs] chore: 생성 및 수정 후 목록으로 이동 처리 (CS_UT03020103) 2025-07-15 12:16:03 +09:00
chanii8 7d6f49987a [apps/cs, common/features] chore: FAQ 페이지 PostPageGuard로 래핑 2025-07-15 12:14:41 +09:00
jyha00 4891725f68 물재이용 페이지 단위 추가 2025-07-15 11:11:29 +09:00
734 changed files with 36256 additions and 17350 deletions
-8
View File
@@ -1,8 +0,0 @@
# 디폴트 무시된 파일
/shelf/
/workspace.xml
# 에디터 기반 HTTP 클라이언트 요청
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
@@ -185,6 +185,15 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
...prev,
[siteId]: value,
}));
// 상시 선택 시 게시 기간 미설정
if (value === "99") {
handleUpdateFields(siteId)({
pstgPrdSeCd: value,
});
return;
}
setRememberPeriodDate((prev) => ({
...prev,
[siteId]: { from, to },
@@ -20,7 +20,7 @@ import {
Skeleton,
} from "@common/components";
import { AttachmentApi } from "@/entities/attachment";
import { PostApi, type PostParams } from "@/entities/post";
import { PostApi, PostPageGuard, type PostParams } from "@/entities/post";
import { useAlertStore, useMenuStore, useUserStore } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
import dayjs from "dayjs";
@@ -103,143 +103,147 @@ function FaqPage({ bbsId }: { bbsId: string }) {
};
return (
<div className="card card-border !mt-6">
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2 text-label">검색</Label>
<Select
value={postParams.keywordType}
onValueChange={(keywordType) => {
setPostParams({ ...postParams, keywordType });
}}
>
<SelectTrigger className="h-[3.2rem] w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="title">제목</SelectItem>
<SelectItem value="all">제목+내용</SelectItem>
<SelectItem value="writer">작성자</SelectItem>
</SelectContent>
</Select>
<Input
value={postParams.keyword}
onChange={(e) =>
setPostParams({ ...postParams, keyword: e.target.value })
}
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
</form>
</div>
<div className="card card-border">
<div className="card-header">
<div className="flex w-full items-center justify-between">
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
<div className="flex gap-2">
{pageMenuAuthority?.regAuthrtYn === "Y" && (
<Button onClick={() => router.push(`${pathname}/register`)}>
등록
</Button>
)}
{pageMenuAuthority?.delAuthrtYn === "Y" && (
<Button
color="red"
onClick={onSelectedPostDelete}
disabled={!selectedPostNumbers.length}
>
선택삭제
</Button>
)}
<PostPageGuard bbsId={bbsId}>
<div className="card card-border !mt-6">
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2 text-label">검색</Label>
<Select
value={postParams.keywordType}
onValueChange={(keywordType) => {
setPostParams({ ...postParams, keywordType });
}}
>
<SelectTrigger className="h-[3.2rem] w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="title">제목</SelectItem>
<SelectItem value="all">제목+내용</SelectItem>
<SelectItem value="writer">작성자</SelectItem>
</SelectContent>
</Select>
<Input
value={postParams.keyword}
onChange={(e) =>
setPostParams({ ...postParams, keyword: e.target.value })
}
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
</form>
</div>
<div className="card card-border">
<div className="card-header">
<div className="flex w-full items-center justify-between">
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
<div className="flex gap-2">
{pageMenuAuthority?.regAuthrtYn === "Y" && (
<Button onClick={() => router.push(`${pathname}/register`)}>
등록
</Button>
)}
{pageMenuAuthority?.delAuthrtYn === "Y" && (
<Button
color="red"
onClick={onSelectedPostDelete}
disabled={!selectedPostNumbers.length}
>
선택삭제
</Button>
)}
</div>
</div>
</div>
</div>
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
<div className="mb-4 flex items-center gap-2 rounded-sm border border-border bg-background p-4">
<Checkbox
id="all-select"
checked={selectedPostNumbers.length === postList?.content.length}
onClick={(e) => e.stopPropagation()}
onCheckedChange={handleAllCheckboxChange}
/>
<Label htmlFor="all-select" className="text-2xl">
전체선택
</Label>
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
<div className="mb-4 flex items-center gap-2 rounded-sm border border-border bg-background p-4">
<Checkbox
id="all-select"
checked={
selectedPostNumbers.length === postList?.content.length
}
onClick={(e) => e.stopPropagation()}
onCheckedChange={handleAllCheckboxChange}
/>
<Label htmlFor="all-select" className="text-2xl">
전체선택
</Label>
</div>
{isFetched ? (
<Accordion
type="multiple"
className="secondary flex flex-col gap-4"
>
{postList?.content.map((item) => (
<div key={item.pstNo} className="flex items-start gap-2">
<Checkbox
id={item.pstNo.toString()}
checked={selectedPostNumbers.includes(item.pstNo)}
onClick={(e) => e.stopPropagation()}
onCheckedChange={(checked) =>
handleCheckboxChange(item.pstNo, checked)
}
className="mx-2 my-6"
/>
<AccordionItem
value={item.pstNo.toString()}
className="w-full"
>
<AccordionTrigger>
<div className="flex flex-1 items-center justify-between">
<div className="flex items-center gap-4">
<span className="text-primary">Q</span>
<span>{item.pstTtl}</span>
</div>
<span className="px-4 text-[1.2rem] font-normal text-label">
등록일: {dayjs(item.regDt).format("YYYY-MM-DD")}
</span>
</div>
</AccordionTrigger>
<AccordionContent>
<div className="flex items-start gap-4">
<span className="text-[1.4rem] font-bold text-label">
A
</span>
<div className="flex-1">
<pre className="whitespace-pre-wrap font-noto text-[1.4rem] tracking-tighter text-foreground">
{item.pstCn}
</pre>
<Attachment atflCd={item.atflCd} />
</div>
<div className="flex justify-end gap-2">
<Button
size="icon"
color="white"
onClick={() =>
router.push(`${pathname}/${item.pstNo}`)
}
>
<i className="diveicon di-pencil" />
</Button>
<Button
size="icon"
color="white"
onClick={() => handleDeletePost(item.pstNo)}
>
<i className="diveicon di-trash" />
</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
</div>
))}
</Accordion>
) : (
<Skeleton className="h-[36rem] w-full" />
)}
</div>
{isFetched ? (
<Accordion
type="multiple"
className="secondary flex flex-col gap-4"
>
{postList?.content.map((item) => (
<div key={item.pstNo} className="flex items-start gap-2">
<Checkbox
id={item.pstNo.toString()}
checked={selectedPostNumbers.includes(item.pstNo)}
onClick={(e) => e.stopPropagation()}
onCheckedChange={(checked) =>
handleCheckboxChange(item.pstNo, checked)
}
className="mx-2 my-6"
/>
<AccordionItem
value={item.pstNo.toString()}
className="w-full"
>
<AccordionTrigger>
<div className="flex flex-1 items-center justify-between">
<div className="flex items-center gap-4">
<span className="text-primary">Q</span>
<span>{item.pstTtl}</span>
</div>
<span className="px-4 text-[1.2rem] font-normal text-label">
등록일: {dayjs(item.regDt).format("YYYY-MM-DD")}
</span>
</div>
</AccordionTrigger>
<AccordionContent>
<div className="flex items-start gap-4">
<span className="text-[1.4rem] font-bold text-label">
A
</span>
<div className="flex-1">
<pre className="whitespace-pre-wrap font-noto text-[1.4rem] tracking-tighter text-foreground">
{item.pstCn}
</pre>
<Attachment atflCd={item.atflCd} />
</div>
<div className="flex justify-end gap-2">
<Button
size="icon"
color="white"
onClick={() =>
router.push(`${pathname}/${item.pstNo}`)
}
>
<i className="diveicon di-pencil" />
</Button>
<Button
size="icon"
color="white"
onClick={() => handleDeletePost(item.pstNo)}
>
<i className="diveicon di-trash" />
</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
</div>
))}
</Accordion>
) : (
<Skeleton className="h-[36rem] w-full" />
)}
</div>
</div>
</div>
</PostPageGuard>
);
}
@@ -0,0 +1,2 @@
import { LinkPage } from "@/pages/link";
export default LinkPage;
@@ -0,0 +1,10 @@
import { ModifyLinkPage as VModifyLinkPage } from "@/pages/link";
export default async function ModifyLinkPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return <VModifyLinkPage linkSn={id} />;
};
@@ -0,0 +1,2 @@
import { RegistLinkPage } from "@/pages/link";
export default RegistLinkPage;
@@ -0,0 +1,2 @@
import { LinkLogPage } from "@/pages/linkLog";
export default LinkLogPage;
@@ -0,0 +1,2 @@
import { LinkParamsPage } from "@/pages/linkParams";
export default LinkParamsPage;
@@ -0,0 +1,2 @@
import { LinkStatsPage } from "@/pages/linkStats";
export default LinkStatsPage;
@@ -0,0 +1,2 @@
import { LinkTablePage } from "@/pages/linkTable";
export default LinkTablePage;
@@ -0,0 +1,2 @@
import { UserStatisticsPage } from "@/pages/user-statistics";
export default UserStatisticsPage;
+27 -8
View File
@@ -1,5 +1,12 @@
import { NextRequest, NextResponse } from "next/server";
import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config";
import {
CLAIM_NAME,
AUTH_USER_ID,
PORT,
PROXY_HOST,
SERVER_API_URL,
SITE_ID,
} from "./src/shared/config";
const serverApiUrl =
process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000";
@@ -7,17 +14,23 @@ const serverApiUrl =
export function middleware(req: NextRequest) {
const pathname = req.nextUrl.pathname;
const searchParams = req.nextUrl.searchParams;
const logMessages: string[] = [
`[Middleware Request] ${new Date().toISOString()}`,
`[Environment]\n Mode: ${process.env.NODE_ENV}\n Port: ${PORT}\n Proxy Host: ${PROXY_HOST}\n Server API URL: ${SERVER_API_URL}\n Site ID: ${SITE_ID}`,
`[Request]\n Original Pathname: ${pathname}`,
];
if (pathname.startsWith("/server")) {
const decodedParams = Array.from(searchParams.entries())
.map(([key, value]) => `${key}=${decodeURIComponent(value)}`)
.join("&");
const queryParams = searchParams.toString();
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
decodedParams ? `?${decodedParams}` : ""
queryParams ? `?${queryParams}` : ""
}`;
logMessages.push(` Rewritten URL: ${newUrl}`);
logMessages.push("\n--------------------------------\n");
console.log(logMessages.join("\n"));
return NextResponse.rewrite(new URL(newUrl));
}
@@ -30,6 +43,10 @@ export function middleware(req: NextRequest) {
if (token) requestHeaders.set(CLAIM_NAME, token);
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
logMessages.push(` Rewritten URL: ${newUrl}`);
logMessages.push("\n--------------------------------\n");
console.log(logMessages.join("\n"));
return NextResponse.rewrite(new URL(newUrl), {
headers: requestHeaders,
});
@@ -39,6 +56,8 @@ export function middleware(req: NextRequest) {
return NextResponse.redirect(new URL("/dashboard", req.url));
}
logMessages.push("\n--------------------------------\n");
console.log(logMessages.join("\n"));
return NextResponse.next();
}
@@ -67,11 +67,22 @@ export const contentsDetailSchema = z.object({
useYn: z.string(),
});
export const contentsFormSchema = contentsDetailSchema.omit({
contsVer: true,
aprvDt: true,
autzr: true,
});
export const contentsFormSchema = contentsDetailSchema
.omit({
contsVer: true,
aprvDt: true,
autzr: true,
})
.extend({
contsSn: z.number().optional(),
aprvCd: z
.enum([
CONTENTS_APPROVE_STATUS.DRAFT,
CONTENTS_APPROVE_STATUS.REQUESTED,
CONTENTS_APPROVE_STATUS.APPROVED,
])
.optional(),
});
export type Contents = z.infer<typeof contentsSchema>;
export type ContentsDetail = z.infer<typeof contentsDetailSchema>;
@@ -0,0 +1,191 @@
import axios from "axios";
import {
type LinkService,
type LinkSearchParam,
type LinkList,
type LinkFormData,
type LinkInfo,
type LinkLogList,
type LinkStatsList,
type LinkVrblList,
type LinkVrblFormData,
type LinkVrbl,
type LinkTable,
type LinkTableColumnsList,
type LinkTableColumns,
type LinkTableColData,
type LinkTableFormData,
} from "../model/link-interface";
export class LinkApiService implements LinkService {
public async getLinkList(params: LinkSearchParam): Promise<LinkList> {
const response = await axios.get<LinkList>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`,
{ params }
);
return response.data;
}
public async getLinkInfo(linkSn: string): Promise<LinkInfo> {
const response = await axios.get<LinkInfo>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkInfo/${linkSn}`,
);
return response.data;
}
public async linkQuickStart(linkSn: string): Promise<string> {
const response = await axios.patch<string>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/linkQuickStart/${linkSn}`,
);
return response.data;
}
public async updateLinkInfo(data: LinkFormData): Promise<void> {
await axios.put<void>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/updateLinkInfo`,
data,
);
}
public async insertLinkInfo(data: LinkFormData): Promise<void> {
await axios.post<void>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/insertLinkInfo`,
data,
);
}
public async deleteLink(linkSn: string): Promise<void> {
await axios.delete<void>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/deleteLink/${linkSn}`,
);
}
public async getLinkLogList(params: LinkSearchParam): Promise<LinkLogList> {
const response = await axios.get<LinkLogList>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/selectLinkLoglist`,
{ params }
);
return response.data;
}
public async getLinkStatsList(params: LinkSearchParam): Promise<LinkStatsList> {
const response = await axios.get<LinkStatsList>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/selectLinkStats`,
{ params }
);
return response.data;
}
public async getLinkVrblList(linkSn: string): Promise<LinkVrblList> {
const response = await axios.get<LinkVrblList>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkVrblList/${linkSn}`,
);
return response.data;
}
public async insertLinkVrbl(data: LinkVrblFormData): Promise<number> {
const response = await axios.post<number>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/insertLinkVrbl`,
data,
);
return response.data;
}
public async deleteLinkVrbl(data: string[]): Promise<void> {
await axios.delete<void>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/deleteLinkVrbl`,
{
data
}
);
}
public async getLinkVrblInfo(linkVrblSn: string): Promise<LinkVrbl> {
const response = await axios.get<LinkVrbl>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkVrblInfo/${linkVrblSn}`,
);
return response.data;
}
public async updateLinkVrblInfo(data: LinkVrblFormData): Promise<void> {
await axios.put<void>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/updateLinkVrblInfo`,
data,
);
}
public async getLinkTableInfo(linkSn: string): Promise<LinkTable> {
const response = await axios.get<LinkTable>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo/${linkSn}`,
);
return response.data;
}
public async getLinkTableColList(linkSn: string): Promise<LinkTableColumnsList> {
const response = await axios.get<LinkTableColumnsList>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList/${linkSn}`,
);
return response.data;
}
public async getLinkTableColInfo(params: { colSn: string; tblSn:string; }): Promise<LinkTableColumns> {
const response = await axios.get<LinkTableColumns>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColInfo`,
{ params }
);
return response.data;
}
public async insertLinkTableCol(data: LinkTableColData): Promise<void> {
await axios.post<void>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/insertLinkTableCol`,
data
);
}
public async deleteLinkTableCol(data: string[]): Promise<void> {
await axios.delete<void>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/deleteLinkTableCol`,
{
data
},
);
}
public async updateLinkTableCol(data: LinkTableColData): Promise<void> {
axios.put<void>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/updateLinkTableCol`,
data,
);
}
public async insertLinkTableInfo(data: LinkTableFormData): Promise<void> {
await axios.post<void>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/insertLinkTableInfo`,
data
);
}
public async updateLinkTableInfo(data: LinkTableFormData): Promise<void> {
await axios.put<void>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/updateLinkTableInfo`,
data
);
}
public async countLinkTblNm(tblNm: string): Promise<{count: number, tbl_sn: string}> {
const response = await axios.get<{count: number, tbl_sn: string}>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/countLinkTblNm/${tblNm}`,
);
return response.data;
}
public async updateLinkResultCode(linkSn: string): Promise<void> {
await axios.patch<void>(
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/updateLinkResultCode/${linkSn}`,
);
}
}
export const LinkApi = new LinkApiService();
+18
View File
@@ -0,0 +1,18 @@
export { LinkApi } from "./api/link-service";
export {
type LinkSearchParam,
type Link,
type LinkFormData,
type LinkLog,
type LinkStats,
type LinkVrbl,
type LinkVrblFormData,
type LinkTable,
type LinkTableColumns,
type LinkTableColData,
type LinkTableFormData,
linkFormSchema,
linkVrblFormSchema,
LinkTableColumnsSchema,
LinkTableSchema,
} from "./model/link-interface"
@@ -0,0 +1,235 @@
import { z } from "zod";
import type { PageableParams, PageableList } from "@/shared/model";
export interface Link {
link_sn: string;
link_nm: string;
link_clsf_cd: string;
link_url: string;
link_srvr_ip: string;
link_db_cd: string;
link_key: string;
link_id: string;
link_pswd: string;
link_cycle_stng: string;
srvc_usg: string;
last_link_rslt_cd: string;
last_link_excn_dt: string;
next_link_excn_dt: string;
last_link_err_cd: string;
mdfcn_dt: string;
mdfr: string;
link_batch_time: string;
}
export interface LinkLog {
rownum: number;
link_sn: string;
link_nm: string;
link_rslt_cd: string;
err_cn: string;
link_err_cd: string;
log_crt_dt: string;
}
export interface LinkInfo extends LinkFormData {
link_sn: string;
link_nm: string;
link_clsf_cd: string;
link_url: string;
link_srvr_ip: string;
link_db_cd: string;
link_key: string;
link_id: string;
link_pswd: string;
srvc_usg: string;
last_link_rslt_cd: string;
last_link_excn_dt: string;
next_link_excn_dt: string;
last_link_err_cd: string;
mdfcn_dt: string;
mdfr: string;
link_batch_time: number;
}
export interface LinkStats {
link_sn: string;
link_nm: string;
count: number;
}
export interface LinkVrbl {
link_vrbl_sn: string;
link_vrbl_nm: string;
link_vrbl_vl: string;
link_vrbl_cd: string;
link_vrbl_expln: string;
mdfcn_dt: string;
mdfr: string;
}
export interface LinkTable {
link_nm: string;
link_sn: string;
tbl_sn: string;
sou_tbl_nm: string;
sou_tbl_korn_nm: string;
sou_tbl_expln: string;
trsf_tbl_nm: string;
trsf_tbl_korn_nm: string;
trsf_tbl_expln: string;
}
export interface LinkTableColumns {
tbl_sn: string;
col_sn: string;
sou_col_nm: string;
sou_col_korn_nm: string;
sou_col_data_frm: string;
sou_col_sz: number;
trsf_col_nm: string;
trsf_col_korn_nm: string;
trsf_col_data_frm: string;
trsf_col_sz: number
pk_yn: string;
col_seq: number;
mdfcn_dt: string;
mdfr: string;
}
export const linkFormSchema = z.object({
link_clsf_cd: z.string().max(8).nullable().refine((val) => {
// 빈 값이나 "none" 값이면 false 반환
return val && val !== "none";
}, {
message: "연계 분류를 선택해주세요"
}),
link_sn: z.string().max(50).nullable().optional(),
link_nm: z.string().max(40, { message: '연계명은 40자 이하로 입력해 주세요.' }).nonempty({ message: '연계명은 필수입니다.' }),
link_url: z.string().max(1000, { message: '연계 URL은 1000자 이하로 입력해 주세요.' }).nonempty({ message: '연계 URL은 필수입니다.' }),
link_srvr_ip: z.string().max(20).nullable().optional(),
link_db_cd: z.string().max(10).nullable().optional(),
link_key: z.string().max(100, { message: '연계 KEY는 100자 이하로 입력해 주세요.' }).nonempty({ message: '연계 KEY는 필수입니다.' }),
link_id: z.string().max(20).nullable().optional(),
link_pswd: z.string().max(128).nullable().optional(),
srvc_usg: z.string().max(40).nullable().optional(),
last_link_rslt_cd: z.string().max(4).nullable().optional(),
last_link_excn_dt: z.string().max(40).nullable().optional(),
next_link_excn_dt: z.string().max(40).nullable().optional(),
last_link_err_cd: z.string().max(10).nullable().optional(),
mdfcn_dt: z.string().nullable().optional(),
mdfr: z.string().nullable().optional(),
link_batch_time: z.coerce.number().nullable().optional().refine((val) => {
if (val == null) return true;
return val >= 5;
}, {
message: "연계 주기는 5분 이상이어야 합니다."
}),
});
export const linkVrblFormSchema = z.object({
link_sn: z.string().max(50).nullable().optional(),
link_vrbl_sn: z.string().max(50).nullable().optional(),
link_vrbl_nm: z.string().max(40, { message: '변수명은 40자 이하로 입력해 주세요.' }).nonempty({ message: '변수명은 필수입니다.' }),
link_vrbl_expln: z.string().max(4000, { message: '설명은 400자 이하로 입력해 주세요.' }).nullable().optional(),
link_vrbl_cd: z.string().max(10, { message: '변수코드는 10자 이하로 입력해 주세요.' }).nullable().optional(),
link_vrbl_vl: z.string().max(1000, { message: '변수값은 1000자 이하로 입력해 주세요.' }).nonempty({ message: '변수값은 필수입니다.' }),
mdfcn_dt: z.string().nullable().optional(),
mdfr: z.string().nullable().optional(),
});
export const LinkTableColumnsSchema = z.object({
link_sn: z.string().max(50).nullable().optional(),
tbl_sn: z.string().max(50).nullable().optional(),
col_sn: z.string().max(50).nullable().optional(),
sou_col_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nullable().optional(),
sou_col_korn_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nullable().optional(),
sou_col_data_frm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nullable().optional(),
sou_col_sz: z.coerce.number().nullable().optional().refine((val) => {
if (val == null) return true;
return val.toString().replace('.', '').length <= 14;
}, {
message: '최대 14자리까지 입력 가능합니다.',
}),
pk_yn: z.string().max(1).nonempty(),
trsf_col_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nonempty({ message: '변환컬럼명은 필수입니다.' }),
trsf_col_korn_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nonempty({ message: '변환컬럼 한글명은 필수입니다.' }),
trsf_col_data_frm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nonempty({ message: '변환컬럼 데이터 형식은 필수입니다.' }),
trsf_col_sz: z.coerce.number().nullable().optional().refine((val) => {
if (val == null) return true;
return val.toString().replace('.', '').length <= 14;
}, {
message: '최대 14자리까지 입력 가능합니다.',
}),
col_seq: z.coerce.number().nullable().optional().refine((val) => {
if (val == null) return true;
return val > 0;
}, {
message: "컬럼 매칭 순서는 0 이상이어야 합니다."
}),
mdfcn_dt: z.string().nullable().optional(),
mdfr: z.string().nullable().optional(),
});
export const LinkTableSchema = z.object({
link_sn: z.string().max(50).nullable().optional(),
tbl_sn: z.string().max(50).nullable().optional(),
link_nm: z.string().max(50).nullable().optional(),
sou_tbl_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nullable().optional(),
sou_tbl_korn_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nullable().optional(),
sou_tbl_expln: z.string().max(2000, { message: '설명은 2000자 이하로 입력해 주세요.' }).nullable().optional(),
trsf_tbl_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nonempty({ message: '변환테이블명은 필수입니다.' }),
trsf_tbl_korn_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nonempty({ message: '변환테이블 한글명은 필수입니다.' }),
trsf_tbl_expln: z.string().max(2000, { message: '설명은 2000자 이하로 입력해 주세요.' }).nullable().optional(),
mdfcn_dt: z.string().nullable().optional(),
mdfr: z.string().nullable().optional(),
});
interface LinkStatsListObject {
linkSuccStats: LinkStats[] | null;
linkFailStats: LinkStats[] | null;
}
export interface LinkSearchParam extends PageableParams {
linkSn?: string;
linkNm?: string;
startDate?: string;
endDate?: string;
isSucc?: string;
}
export type LinkFormData = z.infer<typeof linkFormSchema>;
export type LinkVrblFormData = z.infer<typeof linkVrblFormSchema>;
export type LinkTableColData = z.infer<typeof LinkTableColumnsSchema>;
export type LinkTableFormData = z.infer<typeof LinkTableSchema>;
export type LinkList = PageableList<Link>;
export type LinkLogList = PageableList<LinkLog>;
export type LinkStatsList = LinkStatsListObject;
export type LinkVrblList = PageableList<LinkVrbl>;
export type LinkTableColumnsList = PageableList<LinkTableColumns>;
export interface LinkService {
getLinkList(params: LinkSearchParam): Promise<LinkList>;
getLinkInfo(params: string): Promise<LinkInfo>;
linkQuickStart(params: string): Promise<string>;
updateLinkInfo(data: LinkFormData): Promise<void>;
deleteLink(params: string): Promise<void>;
getLinkLogList(params: LinkSearchParam): Promise<LinkLogList>;
getLinkStatsList(params: LinkSearchParam): Promise<LinkStatsList>;
getLinkVrblList(params: string): Promise<LinkVrblList>;
insertLinkVrbl(data: LinkVrblFormData): Promise<number>;
deleteLinkVrbl(data: string[]): Promise<void>;
getLinkVrblInfo(params: string): Promise<LinkVrbl>;
updateLinkVrblInfo(data: LinkVrblFormData): Promise<void>;
getLinkTableInfo(params: string): Promise<LinkTable>;
getLinkTableColList(params: string): Promise<LinkTableColumnsList>;
getLinkTableColInfo(params: { colSn: string; tblSn: string; }): Promise<LinkTableColumns>;
insertLinkTableCol(data: LinkTableColData): Promise<void>;
deleteLinkTableCol(data: string[]): Promise<void>;
updateLinkTableCol(data: LinkTableColData): Promise<void>;
insertLinkTableInfo(data: LinkTableFormData): Promise<void>;
updateLinkTableInfo(data: LinkTableFormData): Promise<void>;
insertLinkInfo(data: LinkFormData): Promise<void>;
countLinkTblNm(tblNm: string): Promise<{ count: number, tbl_sn: string }>;
updateLinkResultCode(linkSn: string): Promise<void>;
}
@@ -6,6 +6,7 @@ import type {
MenuItem,
MenuFormData,
MenuTemporarilyStoredData,
SiteMapFormData,
} from "../model/menu-interface";
export class MenuApiService implements MenuService {
@@ -65,6 +66,14 @@ export class MenuApiService implements MenuService {
);
return response.data;
}
public async publishSiteMap(data: SiteMapFormData["siteMap"]): Promise<void> {
const response = await client.post<void>(
`${ENDPOINT.CMS_SERVICE.MENUS}/sitemap/publish`,
data,
);
return response.data;
}
}
export const MenuApi = new MenuApiService();
+3
View File
@@ -2,8 +2,11 @@ export { MenuApi } from "./api/menu-service";
export {
MENU_TYPE,
menuFormSchema,
siteMapFormSchema,
type MenuType,
type MenuItem,
type MenuFormData,
type SiteMapFormData,
type SiteMapPayloadData,
} from "./model/menu-interface";
export { MenuWrapper } from "./lib/menu-wrapper";
@@ -72,8 +72,19 @@ export const menuFormSchema = z.object({
menuPicDsgnYmd: z.string().nullish(),
});
export type MenuFormData = z.infer<typeof menuFormSchema>;
export const siteMapPayloadSchema = z.object({
menuCd: z.number(),
menuSubKornNm: z.string(),
otptYn: z.string(),
});
export const siteMapFormSchema = z.object({
siteMap: z.array(siteMapPayloadSchema),
});
export type MenuFormData = z.infer<typeof menuFormSchema>;
export type SiteMapFormData = z.infer<typeof siteMapFormSchema>;
export type SiteMapPayloadData = z.infer<typeof siteMapPayloadSchema>;
export type Menu = Omit<MenuFormData, "iconNm"> & {
menuCd: number;
iconName?: string;
@@ -92,4 +103,5 @@ export interface MenuService {
getMenuTemporarilyStoredData: (
menuCd: number,
) => Promise<MenuTemporarilyStoredData[]>;
publishSiteMap: (data: SiteMapFormData["siteMap"]) => Promise<void>;
}
@@ -194,6 +194,15 @@ function PostingDialog({
...prev,
[siteId]: value,
}));
// 상시 선택 시 게시 기간 미설정
if (value === "99") {
handleUpdateFields(siteId)({
pstgPrdSeCd: value,
});
return;
}
setRememberPeriodDate((prev) => ({
...prev,
[siteId]: { from, to },
+2
View File
@@ -0,0 +1,2 @@
export { TreeMenuApi } from "./api/tree-menu-service";
export type { TreeMenu, TreeNode } from "./model/tree-menu-interface";
@@ -19,6 +19,7 @@ export interface TreeMenu extends Omit<TreeNode, "name" | "icon" | "children"> {
qrcd?: string;
useYn: string;
otptYn: string;
menuSubKornNm?: string;
requiredUrlPath?: boolean;
}
+1
View File
@@ -4,6 +4,7 @@ export {
userSaveSchema,
userPasswordUpdateSchema,
userUpdateSchema,
type UserStateCode,
type UserSearchParams,
type UserListResponse,
type UserSaveData,
@@ -9,7 +9,8 @@ export const USER_STATE_CODE = {
삭제: "09",
} as const;
type UserStateCode = (typeof USER_STATE_CODE)[keyof typeof USER_STATE_CODE];
export type UserStateCode =
(typeof USER_STATE_CODE)[keyof typeof USER_STATE_CODE];
// 사용자 검색 파라미터
export interface UserSearchParams extends PageableParams {
+137 -132
View File
@@ -23,6 +23,7 @@ import {
import { AttachmentApi } from "@/entities/attachment";
import {
PostApi,
PostPageGuard,
deleteReasonMessages,
type PostParams,
} from "@/entities/post";
@@ -120,143 +121,147 @@ function FaqPage({ bbsId }: { bbsId: string }) {
};
return (
<div className="card card-border !mt-6">
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2 text-label">검색</Label>
<Select
value={postParams.keywordType}
onValueChange={(keywordType) => {
setPostParams({ ...postParams, keywordType });
}}
>
<SelectTrigger className="h-[3.2rem] w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="title">제목</SelectItem>
<SelectItem value="all">제목+내용</SelectItem>
<SelectItem value="writer">작성자</SelectItem>
</SelectContent>
</Select>
<Input
value={postParams.keyword}
onChange={(e) =>
setPostParams({ ...postParams, keyword: e.target.value })
}
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
</form>
</div>
<div className="card card-border">
<div className="card-header">
<div className="flex w-full items-center justify-between">
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
<div className="flex gap-2">
<AuthorityButton
authority="regAuthrtYn"
onClick={() => router.push(`${pathname}/register`)}
>
등록
</AuthorityButton>
<AuthorityButton
authority="delAuthrtYn"
color="red"
onClick={onSelectedPostDelete}
disabled={!selectedPostNumbers.length}
>
선택삭제
</AuthorityButton>
<PostPageGuard bbsId={bbsId}>
<div className="card card-border !mt-6">
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2 text-label">검색</Label>
<Select
value={postParams.keywordType}
onValueChange={(keywordType) => {
setPostParams({ ...postParams, keywordType });
}}
>
<SelectTrigger className="h-[3.2rem] w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="title">제목</SelectItem>
<SelectItem value="all">제목+내용</SelectItem>
<SelectItem value="writer">작성자</SelectItem>
</SelectContent>
</Select>
<Input
value={postParams.keyword}
onChange={(e) =>
setPostParams({ ...postParams, keyword: e.target.value })
}
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
</form>
</div>
<div className="card card-border">
<div className="card-header">
<div className="flex w-full items-center justify-between">
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
<div className="flex gap-2">
<AuthorityButton
authority="regAuthrtYn"
onClick={() => router.push(`${pathname}/register`)}
>
등록
</AuthorityButton>
<AuthorityButton
authority="delAuthrtYn"
color="red"
onClick={onSelectedPostDelete}
disabled={!selectedPostNumbers.length}
>
선택삭제
</AuthorityButton>
</div>
</div>
</div>
</div>
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
<div className="mb-4 flex items-center gap-2 rounded-sm border border-border bg-background p-4">
<Checkbox
id="all-select"
checked={selectedPostNumbers.length === postList?.content.length}
onClick={(e) => e.stopPropagation()}
onCheckedChange={handleAllCheckboxChange}
/>
<Label htmlFor="all-select" className="text-2xl">
전체선택
</Label>
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
<div className="mb-4 flex items-center gap-2 rounded-sm border border-border bg-background p-4">
<Checkbox
id="all-select"
checked={
selectedPostNumbers.length === postList?.content.length
}
onClick={(e) => e.stopPropagation()}
onCheckedChange={handleAllCheckboxChange}
/>
<Label htmlFor="all-select" className="text-2xl">
전체선택
</Label>
</div>
{isFetched ? (
<Accordion
type="multiple"
className="secondary flex flex-col gap-4"
>
{postList?.content.map((item) => (
<div key={item.pstNo} className="flex items-start gap-2">
<Checkbox
id={item.pstNo.toString()}
checked={selectedPostNumbers.includes(item.pstNo)}
onClick={(e) => e.stopPropagation()}
onCheckedChange={(checked) =>
handleCheckboxChange(item.pstNo, checked)
}
className="mx-2 my-6"
/>
<AccordionItem
value={item.pstNo.toString()}
className="w-full"
>
<AccordionTrigger>
<div className="flex flex-1 items-center justify-between">
<div className="flex items-center gap-4">
<span className="text-primary">Q</span>
<span>{item.pstTtl}</span>
</div>
<span className="px-4 text-[1.2rem] font-normal text-label">
등록일: {dayjs(item.regDt).format("YYYY-MM-DD")}
</span>
</div>
</AccordionTrigger>
<AccordionContent>
<div className="flex items-start gap-4">
<span className="text-[1.4rem] font-bold text-label">
A
</span>
<div className="flex-1">
<pre className="whitespace-pre-wrap font-noto text-[1.4rem] tracking-tighter text-foreground">
{item.pstCn}
</pre>
<Attachment atflCd={item.atflCd} />
</div>
<div className="flex justify-end gap-2">
<Button
size="icon"
color="white"
onClick={() =>
router.push(`${pathname}/${item.pstNo}`)
}
>
<i className="diveicon di-pencil" />
</Button>
<Button
size="icon"
color="white"
onClick={() => handleDeletePost(item.pstNo)}
>
<i className="diveicon di-trash" />
</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
</div>
))}
</Accordion>
) : (
<Skeleton className="h-[36rem] w-full" />
)}
</div>
{isFetched ? (
<Accordion
type="multiple"
className="secondary flex flex-col gap-4"
>
{postList?.content.map((item) => (
<div key={item.pstNo} className="flex items-start gap-2">
<Checkbox
id={item.pstNo.toString()}
checked={selectedPostNumbers.includes(item.pstNo)}
onClick={(e) => e.stopPropagation()}
onCheckedChange={(checked) =>
handleCheckboxChange(item.pstNo, checked)
}
className="mx-2 my-6"
/>
<AccordionItem
value={item.pstNo.toString()}
className="w-full"
>
<AccordionTrigger>
<div className="flex flex-1 items-center justify-between">
<div className="flex items-center gap-4">
<span className="text-primary">Q</span>
<span>{item.pstTtl}</span>
</div>
<span className="px-4 text-[1.2rem] font-normal text-label">
등록일: {dayjs(item.regDt).format("YYYY-MM-DD")}
</span>
</div>
</AccordionTrigger>
<AccordionContent>
<div className="flex items-start gap-4">
<span className="text-[1.4rem] font-bold text-label">
A
</span>
<div className="flex-1">
<pre className="whitespace-pre-wrap font-noto text-[1.4rem] tracking-tighter text-foreground">
{item.pstCn}
</pre>
<Attachment atflCd={item.atflCd} />
</div>
<div className="flex justify-end gap-2">
<Button
size="icon"
color="white"
onClick={() =>
router.push(`${pathname}/${item.pstNo}`)
}
>
<i className="diveicon di-pencil" />
</Button>
<Button
size="icon"
color="white"
onClick={() => handleDeletePost(item.pstNo)}
>
<i className="diveicon di-trash" />
</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
</div>
))}
</Accordion>
) : (
<Skeleton className="h-[36rem] w-full" />
)}
</div>
</div>
</div>
</PostPageGuard>
);
}
@@ -47,7 +47,7 @@ function AddAdminDialog() {
const [open, setOpen] = useState(false);
const [userSearchQuery, setUserSearchQuery] = useState<UserSearchParams>({
keywordType: "",
keywordType: "userName",
keyword: "",
});
const [selectedGroupId, setSelectedGroupId] = useState("");
@@ -193,6 +193,7 @@ function AddAdminDialog() {
<form onSubmit={handleSearch} className="flex items-center gap-2">
<Select
value={userSearchQuery.keywordType}
onValueChange={(value) =>
setUserSearchQuery({ ...userSearchQuery, keywordType: value })
}
@@ -201,8 +202,8 @@ function AddAdminDialog() {
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="userId">아이디</SelectItem>
<SelectItem value="userName">이름</SelectItem>
<SelectItem value="userId">아이디</SelectItem>
</SelectContent>
</Select>
<Input
+1 -1
View File
@@ -38,7 +38,7 @@ function AdminPage() {
const alert = useAlertStore((state) => state.setMessage);
const { confirm } = useConfirm();
const [keywordType, setKeywordType] = useState("");
const [keywordType, setKeywordType] = useState("userId");
const [keyword, setKeyword] = useState("");
const [grpId, setGrpId] = useState("");
const [searchParams, setSearchParams] = useState<UserSearchParams>({
+29 -24
View File
@@ -6,7 +6,6 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import {
Button,
DateRangePicker,
Input,
Label,
Select,
@@ -15,14 +14,14 @@ import {
SelectTrigger,
SelectValue,
} from "@common/components";
import { ApiLogApi } from "@/entities/apilog";
import { type ApiLogParams } from "@/entities/apilog/model/apilog-interface";
import { columns } from "@/pages/apilog/model/table-columns";
import { ApiLogApi, type ApiLogParams } from "@/entities/apilog";
import { useDataTable } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
import { columns } from "@/pages/apilog/model/table-columns";
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
type DateRange = {
from: Date | undefined;
from?: Date | undefined;
to?: Date | undefined;
};
@@ -36,6 +35,7 @@ function ApiLogPage() {
pageIndex: 0,
pageSize: 10,
});
const [presetDateRange, setPresetDateRange] = useState("");
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
const [userId, setUserId] = useState("");
const [apiHttpPmhd, setApiHttpPmhd] = useState("");
@@ -85,8 +85,10 @@ function ApiLogPage() {
...searchParams,
userId,
apiHttpPmhd,
dmndDtStart: dayjs(dateRange.from).format("YYYY-MM-DD"),
dmndDtEnd: dayjs(dateRange.to).format("YYYY-MM-DD"),
dmndDtStart: dateRange.from
? dayjs(dateRange.from).format("YYYY-MM-DD")
: "",
dmndDtEnd: dateRange.to ? dayjs(dateRange.to).format("YYYY-MM-DD") : "",
});
};
@@ -127,31 +129,34 @@ function ApiLogPage() {
default:
presetDateRange = undefined;
}
setPresetDateRange(value);
setDateRange(presetDateRange || defaultDateRange);
};
const handleDateRangeChange = (dateRange: DateRange) => {
setDateRange(dateRange);
setPresetDateRange("");
};
return (
<div className="flex-1">
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2">조회기간</Label>
<DateRangePicker
value={dateRange}
onValueChange={(value) => setDateRange(value || defaultDateRange)}
>
<div className="card card-border flex items-center gap-2">
<Select onValueChange={handlePresetDateRange}>
<SelectTrigger>
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1W">일주일</SelectItem>
<SelectItem value="1M">1개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem>
</SelectContent>
</Select>
</div>
</DateRangePicker>
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1W">일주일</SelectItem>
<SelectItem value="1M">1개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem>
</SelectContent>
</Select>
<InputDateRangePicker
dateRange={dateRange}
setDateRange={handleDateRangeChange}
/>
<Label className="ml-4 mr-2 text-label">사용자ID</Label>
<Input
value={userId}
@@ -35,9 +35,14 @@ export const columns: ColumnDef<Post>[] = [
accessorKey: "pstgPrdSeNm",
header: "게시기간",
cell: ({ row }) => {
const pstgPrdSeCd = row.original.pstgPrdSeCd;
const pstgPrdSeNm = row.original.pstgPrdSeNm;
const pstgBgngDt = row.original.pstgBgngDt;
const pstgEndDt = row.original.pstgEndDt;
if (pstgPrdSeCd === "99") {
return pstgPrdSeNm;
}
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
},
},
@@ -34,7 +34,7 @@ function CreatePage() {
<div className="card-header">
<h3 className="text-2xl font-medium text-label">콘텐츠 등록</h3>
</div>
<ContentsForm form={form} handleSave={createContents} />
<ContentsForm isEditable form={form} handleSave={createContents} />
</div>
);
}
+13 -7
View File
@@ -1,5 +1,6 @@
"use client";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
@@ -24,6 +25,7 @@ import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import { ContentsForm } from "./contents-form";
function ModifyPage({ sequential }: { sequential: string }) {
const router = useRouter();
const { confirm } = useConfirm();
const queryClient = useQueryClient();
const alert = useAlertStore((state) => state.setMessage);
@@ -51,9 +53,11 @@ function ModifyPage({ sequential }: { sequential: string }) {
mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data),
onSuccess: () => {
setContsVer(0);
alert("수정되었습니다.");
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
alert("수정되었습니다.", () => {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
});
router.back();
});
},
});
@@ -62,7 +66,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
mutationFn: (data: ContentsFormData) =>
ContentsApi.updateContents(contsSn, contsVer, data),
onSuccess: () => {
alert("수정되었습니다.");
alert("수정되었습니다.", router.back);
},
});
@@ -70,9 +74,11 @@ function ModifyPage({ sequential }: { sequential: string }) {
mutationFn: () => ContentsApi.deleteContents(contsSn, contsVer),
onSuccess: () => {
setContsVer(0);
alert("삭제되었습니다.");
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
alert("삭제되었습니다.", () => {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
});
router.back();
});
},
});
@@ -0,0 +1,14 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { Dashboard, DashboardService } from "../model/dashboard-interface";
export class DashboardApiService implements DashboardService {
public async getDashboard(): Promise<Dashboard> {
const response = await client.get<Dashboard>(
ENDPOINT.CMS_SERVICE.DASHBOARD,
);
return response.data;
}
}
export const DashboardApi = new DashboardApiService();
+49
View File
@@ -0,0 +1,49 @@
import type { ChartData, ChartDataset } from "chart.js";
import { getCssVariableValue } from "@common/components";
import type {
MenuStatisticsContent,
SiteStatisticsContent,
} from "../model/dashboard-interface";
export function processSiteAccessData(
siteAccessList: SiteStatisticsContent[],
): ChartData<"line">["datasets"] {
const siteGroups: Record<string, ChartDataset> = siteAccessList.reduce(
(acc, item) => {
if (!acc[item.siteNm]) {
acc[item.siteNm] = {
label: item.siteNm,
data: Array(24).fill(0), // 24시간 데이터 초기화
borderColor: getCssVariableValue(
`--chart-${Math.floor(Math.random() * 5) + 1}`,
),
};
}
// 시간대별 접속 카운트 누적
const hourIndex = parseInt(item.accessHour, 10);
if (!isNaN(hourIndex) && hourIndex >= 0 && hourIndex < 24) {
acc[item.siteNm]!.data[hourIndex] = item.accessCnt;
}
return acc;
},
{} as Record<string, ChartDataset>,
);
return Object.values(siteGroups) as ChartData<"line">["datasets"];
}
export function processMenuAccessData(
menuAccessList: MenuStatisticsContent[],
): ChartData<"bar">["datasets"] {
const data = menuAccessList.map((item) => item.accessCnt);
return [
{
label: "메뉴별 접속 횟수",
data,
backgroundColor: "rgb(32, 148, 250)",
},
] as ChartData<"bar">["datasets"];
}
@@ -0,0 +1,172 @@
import { type ChartData, type ChartOptions } from "chart.js";
const commonData = {
labels: ["1", "2", "3", "4", "5", "6", "7"],
datasets: [
{
fill: true,
pointRadius: 0,
tension: 0.5,
},
],
};
const commonOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
scales: {
x: {
display: false,
},
y: {
display: false,
},
},
};
export const chartData: Record<
string,
{ data: ChartData<"line" | "bar">; options: ChartOptions<"line" | "bar"> }
> = {
memberChartData: {
data: {
...commonData,
datasets: [
{
...commonData.datasets[0],
data: [8, 10, 7, 12, 9, 13, 12],
borderColor: "rgba(255, 255, 255, 0.6)",
backgroundColor: "rgba(255, 255, 255, 0.1)",
},
],
},
options: {
...commonOptions,
scales: {
...commonOptions.scales,
y: {
...commonOptions.scales.y,
min: 5,
max: 15,
},
},
},
},
postChartData: {
data: {
...commonData,
datasets: [
{
...commonData.datasets[0],
data: [5200, 5350, 5450, 5520, 5600, 5650, 5678],
borderColor: "rgba(86, 138, 255, 1)",
backgroundColor: "rgba(86, 138, 255, 0.1)",
},
],
},
options: {
...commonOptions,
},
},
visitorChartData: {
data: {
...commonData,
datasets: [
{
...commonData.datasets[0],
data: [1800, 2100, 1950, 2300, 2150, 2250, 2345],
borderColor: "rgba(149, 131, 252)",
backgroundColor: "rgba(149, 131, 252, 0.1)",
},
],
},
options: {
...commonOptions,
},
},
apiChartData: {
data: {
...commonData,
datasets: [
{
...commonData.datasets[0],
data: [41000, 42500, 43200, 44100, 44800, 45200, 45678],
borderColor: "rgba(220, 103, 206)",
backgroundColor: "rgba(220, 103, 206, 0.1)",
},
],
},
options: {
...commonOptions,
},
},
siteAccessChartData: {
data: {
labels: Array.from({ length: 24 }, (_, i) =>
i.toString().padStart(2, "0"),
),
datasets: [],
},
options: {
...commonOptions,
plugins: {
legend: {
position: "top",
},
tooltip: {
mode: "index",
intersect: false,
},
},
scales: {
x: {
title: {
text: "시간대",
},
},
y: {
title: {
text: "접속자 수",
},
beginAtZero: true,
},
},
},
},
menuAccessChartData: {
data: {
labels: [],
datasets: [],
},
options: {
...commonOptions,
plugins: {
legend: {
position: "top",
},
tooltip: {
mode: "index",
intersect: false,
},
},
scales: {
x: {
title: {
text: "메뉴명",
},
},
y: {
title: {
text: "접속 횟수",
},
beginAtZero: true,
},
},
},
},
};
@@ -0,0 +1,32 @@
export interface SiteStatisticsContent {
siteId: string;
siteNm: string;
accessHour: string;
accessCnt: number;
}
export interface MenuStatisticsContent {
menuCd: number;
menuNm: string;
accessCnt: number;
}
export interface Dashboard {
memberTotCnt: number;
memberTodayCnt: number;
pstTotCnt: number;
pstTodayCnt: number;
accessTodayCnt: number;
accessBefDayPer: number;
apiTodayCnt: number;
waitMemberCnt: number;
waitCntnCnt: number;
notiList: [];
schdlList: [];
siteAccessList: SiteStatisticsContent[];
menuAccessList: MenuStatisticsContent[];
}
export interface DashboardService {
getDashboard(): Promise<Dashboard>;
}
@@ -0,0 +1,74 @@
import dayjs from "dayjs";
import type { ColumnDef } from "@tanstack/react-table";
import { Badge } from "@common/components";
import type { RecentPost } from "@/entities/post";
const noticePostTableColumns: ColumnDef<RecentPost>[] = [
{
header: "번호",
cell: ({ row, table }) => {
if (row.original.upndFixYn === "Y") {
return (
<Badge
color="orange"
className="flex size-8 items-center justify-center"
>
<i className="diveicon di-volume-up-fill" />
</Badge>
);
}
const totalRows = table.getRowCount();
const currentPage = table.getState().pagination.pageIndex;
const pageSize = table.getState().pagination.pageSize;
return totalRows - (currentPage * pageSize + row.index);
},
},
{
accessorKey: "pstTtl",
header: "제목",
},
{
accessorKey: "wrtrNm",
header: "작성자",
},
{
accessorKey: "regDt",
header: "작성일",
cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"),
},
{
accessorKey: "inqNocs",
header: "조회수",
},
];
const recentPostTableColumns: ColumnDef<RecentPost>[] = [
{
header: "번호",
cell: ({ row, table }) => {
const totalRows = table.getRowCount();
const currentPage = table.getState().pagination.pageIndex;
const pageSize = table.getState().pagination.pageSize;
return totalRows - (currentPage * pageSize + row.index);
},
},
{
accessorKey: "pstTtl",
header: "제목",
},
{
accessorKey: "wrtrNm",
header: "작성자",
},
{
accessorKey: "regDt",
header: "작성일",
cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"),
},
{
accessorKey: "inqNocs",
header: "조회수",
},
];
export { noticePostTableColumns, recentPostTableColumns };
@@ -1,5 +1,433 @@
"use client";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { getCoreRowModel } from "@tanstack/react-table";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
Filler,
Legend,
Tooltip,
type ChartData,
} from "chart.js";
import { Bar, Line } from "react-chartjs-2";
import { PostApi, type RecentPost } from "@/entities/post";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import { DashboardApi } from "../api/dashboard-service";
import { chartData } from "../model/chart-data";
import {
noticePostTableColumns,
recentPostTableColumns,
} from "../model/table-columns";
import { processMenuAccessData, processSiteAccessData } from "../lib/utils";
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
Filler,
Legend,
Tooltip,
);
function DashboardPage() {
return <></>;
const router = useRouter();
const { data } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.DASHBOARD],
queryFn: () => DashboardApi.getDashboard(),
});
const noticePostQuery = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS],
queryFn: () =>
PostApi.getRecentPostList({
page: 0,
size: 5,
keywordType: "bbsTypeCd",
keyword: "BBSNOTICE",
}),
});
const recentPostQuery = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS],
queryFn: () =>
PostApi.getRecentPostList({
page: 0,
size: 5,
}),
});
const handleNoticePostRowClick = (row: RecentPost) => {
router.push(`/bbs/bbsnotice/${row.bbsId}/${row.pstNo}`);
};
const handleRecentPostRowClick = (row: RecentPost) => {
router.push(
`/bbs/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`,
);
};
const { tableComponent: noticePostTableComponent } = useDataTable({
query: noticePostQuery,
tableOptions: {
columns: noticePostTableColumns,
getCoreRowModel: getCoreRowModel(),
},
dataTableOptions: {
variant: "line",
onRowClick: handleNoticePostRowClick,
},
});
const { tableComponent: recentPostTableComponent } = useDataTable({
query: recentPostQuery,
tableOptions: {
columns: recentPostTableColumns,
getCoreRowModel: getCoreRowModel(),
},
dataTableOptions: {
variant: "line",
onRowClick: handleRecentPostRowClick,
},
});
return (
<div className="p-6">
<div className="flex flex-col gap-1">
{/* 첫번째 라인 */}
<div className="grid grid-cols-4 gap-1">
{/* 총 회원수 카드 */}
<div className="card card-border h-80 !bg-primary !p-0">
<div className="relative h-full">
<div className="absolute left-0 top-0 p-6 tracking-tighter text-white">
<h3 className="text-2xl font-medium">총 회원수</h3>
<div className="mt-2 text-6xl font-bold">
{data?.memberTotCnt
? data.memberTotCnt.toLocaleString()
: "-"}
</div>
<p className="mt-1 text-2xl opacity-80">
금일 신규 가입자
{data?.memberTodayCnt
? data.memberTodayCnt.toLocaleString()
: "-"}
명
</p>
</div>
{/* 차트 영역 */}
<div className="h-full">
<Line
data={chartData.memberChartData!.data as ChartData<"line">}
options={chartData.memberChartData!.options}
/>
</div>
</div>
</div>
{/* 전체 게시물 카드 */}
<div className="card card-border h-80 !p-0">
<div className="relative h-full">
<div className="absolute left-0 top-0 p-6 tracking-tighter">
<h3 className="text-2xl font-medium">전체 게시물</h3>
<div className="mt-2 text-6xl font-bold">
{data?.pstTotCnt ? data.pstTotCnt.toLocaleString() : "-"}
</div>
<p className="mt-1 text-2xl opacity-80">
금일 +
{data?.pstTodayCnt ? data.pstTodayCnt.toLocaleString() : "-"}{" "}
등록
</p>
</div>
{/* 차트 영역 */}
<div className="h-full">
<Line
data={chartData.postChartData!.data as ChartData<"line">}
options={chartData.postChartData!.options}
/>
</div>
</div>
</div>
{/* 오늘 접속자 수 카드 */}
<div className="card card-border h-80 !p-0">
<div className="relative h-full">
<div className="absolute left-0 top-0 p-6 tracking-tighter">
<h3 className="text-2xl font-medium">오늘 접속자 수</h3>
<div className="mt-2 text-6xl font-bold">
{data?.accessTodayCnt
? data.accessTodayCnt.toLocaleString()
: "-"}
</div>
<p className="mt-1 text-2xl opacity-80">
전일 대비 +
{data?.accessBefDayPer
? data.accessBefDayPer.toLocaleString()
: "-"}
%
</p>
</div>
{/* 차트 영역 */}
<div className="h-full">
<Line
data={chartData.visitorChartData!.data as ChartData<"line">}
options={chartData.visitorChartData!.options}
/>
</div>
</div>
</div>
{/* API 호출 건수 카드 */}
<div className="card card-border h-80 !p-0">
<div className="relative h-full">
<div className="absolute left-0 top-0 p-6 tracking-tighter">
<h3 className="text-2xl font-medium">API 호출 건수</h3>
<div className="mt-2 text-6xl font-bold">
{data?.apiTodayCnt ? data.apiTodayCnt.toLocaleString() : "-"}
</div>
<p className="mt-1 text-2xl opacity-80">금일 누적</p>
</div>
{/* 차트 영역 */}
<div className="h-full">
<Line
data={chartData.apiChartData!.data as ChartData<"line">}
options={chartData.apiChartData!.options}
/>
</div>
</div>
</div>
</div>
{/* 두번째 라인 */}
<div className="grid grid-cols-2 gap-1">
{/* 승인 대기 알림 */}
<div className="card card-border">
<div className="card-header">
<h3 className="text-2xl font-medium">승인 대기 알림</h3>
</div>
<div className="p-4">
<div className="flex gap-4">
{/* 사용자 승인 */}
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6">
<div className="flex items-center">
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-lg bg-red-100">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-red-500"
>
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
</div>
<span className="text-2xl font-medium">사용자 승인</span>
</div>
<div className="mt-5">
<div className="text-5xl font-bold">
{data?.waitMemberCnt
? data.waitMemberCnt.toLocaleString()
: "0"}
건
</div>
<div className="mt-2 text-2xl text-gray-500">
신규 가입 승인 대기
</div>
</div>
</div>
{/* 매뉴 승인 */}
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6">
<div className="flex items-center">
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-blue-500"
>
<rect x="3" y="3" width="7" height="7"></rect>
<rect x="14" y="3" width="7" height="7"></rect>
<rect x="14" y="14" width="7" height="7"></rect>
<rect x="3" y="14" width="7" height="7"></rect>
</svg>
</div>
<span className="text-2xl font-medium">메뉴 승인</span>
</div>
<div className="mt-5">
<div className="text-5xl font-bold">
{(3).toLocaleString()}건
</div>
<div className="mt-2 text-2xl text-gray-500">
매뉴 수정 승인 대기
</div>
</div>
</div>
{/* 컨텐츠 승인 */}
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6">
<div className="flex items-center">
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-lg bg-yellow-100">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-yellow-500"
>
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
</svg>
</div>
<span className="text-2xl font-medium">컨텐츠 승인</span>
</div>
<div className="mt-5">
<div className="text-5xl font-bold">
{data?.waitCntnCnt
? data.waitCntnCnt.toLocaleString()
: "0"}
건
</div>
<div className="mt-2 text-2xl text-gray-500">
컨텐츠 수정 승인 대기
</div>
</div>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-1">
{/* 실시간 알림 */}
<div className="card card-border">
<div className="card-header">
<h3 className="text-2xl font-medium">실시간 알림</h3>
</div>
<div className="p-4">
<div className="space-y-4">
<div className="flex items-center justify-between border-b border-border pb-4">
<span className="font-medium">신규 사용자 가입</span>
<span>{(1).toLocaleString()}분 전</span>
</div>
<div className="flex items-center justify-between border-b border-border pb-4">
<span className="font-medium">메뉴 수정 요청</span>
<span>{(5).toLocaleString()}분 전</span>
</div>
<div className="flex items-center justify-between border-b border-border pb-4">
<span className="font-medium">게시물 신고</span>
<span>{(10).toLocaleString()}분 전</span>
</div>
<div className="flex items-center justify-between">
<span className="font-medium">컨텐츠 승인 요청</span>
<span>{(15).toLocaleString()}분 전</span>
</div>
</div>
</div>
</div>
{/* 일정 알림 */}
<div className="card card-border">
<div className="card-header">
<h3 className="text-2xl font-medium">일정 알림</h3>
</div>
<div className="p-4">
<div className="space-y-4">
<div className="flex items-center justify-between border-b border-border pb-4">
<span className="font-medium">
{(3).toLocaleString()}월 정기 시스템 점검
</span>
<span>내일</span>
</div>
<div className="flex items-center justify-between border-b border-border pb-4">
<span className="font-medium">서버 업데이트 예정</span>
<span>{(3).toLocaleString()}일 후</span>
</div>
<div className="flex items-center justify-between border-b border-border pb-4">
<span className="font-medium">백업 스케줄</span>
<span>{(5).toLocaleString()}일 후</span>
</div>
<div className="flex items-center justify-between">
<span className="font-medium">정기 보안 점검</span>
<span>{(7).toLocaleString()}일 후</span>
</div>
</div>
</div>
</div>
</div>
</div>
{/* 세번째 라인 */}
<div className="grid grid-cols-2 gap-1">
{/* 접속자 통계 */}
<div className="card card-border">
<div className="card-header">
<h3 className="text-2xl font-medium">접속자 통계</h3>
</div>
<div className="h-80 p-4">
<Line
data={{
...chartData.siteAccessChartData!.data,
datasets: processSiteAccessData(data?.siteAccessList ?? []),
}}
options={chartData.siteAccessChartData!.options}
/>
</div>
</div>
{/* 메뉴별 접속 통계 */}
<div className="card card-border">
<div className="card-header">
<h3 className="text-2xl font-medium">메뉴별 접속 통계</h3>
</div>
<div className="h-80 p-4">
<Bar
data={{
labels:
data?.menuAccessList?.map((item) => item.menuNm) || [],
datasets: processMenuAccessData(data?.menuAccessList || []),
}}
options={chartData.menuAccessChartData!.options}
/>
</div>
</div>
</div>
{/* 네번째 라인 */}
<div className="grid grid-cols-2 gap-1">
{/* 공지사항 */}
<div className="card card-border !p-0">
<div className="card-header !m-0 !border-none">
<h3 className="text-2xl font-medium">공지사항</h3>
</div>
{noticePostTableComponent}
</div>
{/* 신규 게시글 */}
<div className="card card-border !p-0">
<div className="card-header !m-0 !border-none">
<h3 className="text-2xl font-medium">신규 게시글</h3>
</div>
{recentPostTableComponent}
</div>
</div>
</div>
</div>
);
}
export { DashboardPage };
+1 -2
View File
@@ -39,8 +39,7 @@ function FilePage() {
mutationFn: (atflId: string) =>
AttachmentApi.deleteAttachmentByAtflId(atflId),
onSuccess: () => {
query.refetch();
alert("삭제되었습니다.");
alert("삭제되었습니다.", query.refetch);
},
});
+56 -20
View File
@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import {
keepPreviousData,
useQuery,
@@ -22,6 +22,7 @@ import {
DialogFooter,
Input,
Checkbox,
Label,
} from "@common/components";
import {
UserApi,
@@ -41,7 +42,8 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
const alert = useAlertStore((state) => state.setMessage);
const [open, setOpen] = useState(false);
const [userSearchQuery, setUserSearchQuery] = useState<UserSearchParams>({
const [userName, setUserName] = useState("");
const [searchParam, setSearchParam] = useState<UserSearchParams>({
userName: "",
});
const [pagination, setPagination] = useState<PaginationState>({
@@ -53,10 +55,10 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
);
const query = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS, userSearchQuery, pagination],
queryKey: [ENDPOINT.USER_SERVICE.USERS, searchParam, pagination],
queryFn: () =>
UserApi.findPageUser({
...userSearchQuery,
...searchParam,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -118,6 +120,9 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
onRowSelectionChange: setUserRowSelection,
getRowId: (row) => row.userUnqId,
},
dataTableOptions: {
viewPagination: true,
},
});
const handleAddUsers = async () => {
@@ -144,10 +149,30 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
}
};
const handleSearch = () => {
setPagination({ pageIndex: 0, pageSize: 10 });
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
e.stopPropagation();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParam({ userName });
};
const resetSearch = () => {
setPagination({
pageIndex: 0,
pageSize: 10,
});
setUserName("");
setSearchParam({ userName: "" });
setUserRowSelection({});
};
useEffect(() => {
if (open) resetSearch();
}, [open]);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
@@ -157,20 +182,31 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
<DialogHeader>
<DialogTitle>사용자 추가</DialogTitle>
</DialogHeader>
<div className="mb-4 flex items-end gap-2">
<Input
value={userSearchQuery.userName}
onChange={(e) =>
setUserSearchQuery({
...userSearchQuery,
userName: e.target.value,
})
}
className="!text-[1.3rem]"
placeholder="사용자 이름으로 검색"
/>
<Button onClick={handleSearch}>검색</Button>
</div>
<form onSubmit={handleSearch} className="mb-4 flex items-end gap-2">
<div className="flex items-center gap-2">
<Label htmlFor="userName" className="mr-2 text-label">
사용자 이름
</Label>
<Input
id="userName"
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
value={userName}
onChange={(e) => setUserName(e.target.value)}
placeholder="사용자 이름으로 검색"
/>
</div>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={resetSearch}
>
초기화
</Button>
</form>
{tableComponent}
<DialogFooter>
<Button color="white" onClick={() => setOpen(false)}>
+2 -2
View File
@@ -161,11 +161,11 @@ function GroupForm({ form }: GroupFormProps) {
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="mngrY" />
<Label htmlFor="mngrY">Y</Label>
<Label htmlFor="mngrY">예</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="N" id="mngrN" />
<Label htmlFor="mngrN">N</Label>
<Label htmlFor="mngrN">아니오</Label>
</div>
</RadioGroup>
</FormControl>
@@ -1,6 +1,6 @@
"use client";
import { memo, useState } from "react";
import { memo, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
@@ -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: !!groupId && open, // open 시점 groupId가 있을 때만 상세 조회 API 호출
});
const { mutateAsync: createGroup } = useMutationWrapper({
@@ -77,7 +77,7 @@ const GroupsDialog = memo(
delYn: group?.delYn ?? "",
authrtId: group?.authrtId ?? "",
siteId: group?.siteId ?? "",
mngrYn: group?.mngrYn ?? "",
mngrYn: group?.mngrYn ?? "Y",
},
});
@@ -91,6 +91,10 @@ const GroupsDialog = memo(
if (await confirm(confirmMessage)) callback(data);
};
useEffect(() => {
if (open) form.reset();
}, [open]);
return (
<Dialog open={open} onOpenChange={setOpen} {...props}>
<DialogTrigger asChild>{children}</DialogTrigger>
@@ -35,9 +35,14 @@ export const columns: ColumnDef<Post>[] = [
accessorKey: "pstgPrdSeNm",
header: "게시기간",
cell: ({ row }) => {
const pstgPrdSeCd = row.original.pstgPrdSeCd;
const pstgPrdSeNm = row.original.pstgPrdSeNm;
const pstgBgngDt = row.original.pstgBgngDt;
const pstgEndDt = row.original.pstgEndDt;
if (pstgPrdSeCd === "99") {
return pstgPrdSeNm;
}
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
},
},
@@ -23,6 +23,7 @@ function CreatePage() {
resolver: zodResolver(imageFormSchema),
defaultValues: {
atflYn: "Y",
useYn: "Y",
},
});
@@ -124,6 +124,7 @@ function ImageForm({ form, imgSn, ...props }: ImageFormProps) {
{...field}
type="number"
min={0}
max={9999}
value={field.value ?? ""}
onChange={(e) =>
field.onChange(parseInt(e.target.value))
+3
View File
@@ -0,0 +1,3 @@
export { ModifyLinkPage } from "./ui/modify-page";
export { RegistLinkPage } from "./ui/regist-page";
export { LinkPage } from "./ui/link-page";
@@ -0,0 +1,88 @@
import dayjs from "dayjs";
import { createColumnHelper } from "@tanstack/react-table";
import { type Link } from "@/entities/link";
import QuickStartButton from "../ui/QuickStartButton";
const columnHelper = createColumnHelper<Link>();
const convertMinutesToPeriod = (minutes: number): string => {
if (!minutes) return '';
const year = Math.floor(minutes / (525600)); // 365일 * 24시간 * 60분
minutes = minutes % 525600;
const month = Math.floor(minutes / (43200)); // 30일 * 24시간 * 60분
minutes = minutes % 43200;
const day = Math.floor(minutes / 1440); // 24시간 * 60분
minutes = minutes % 1440;
const hour = Math.floor(minutes / 60);
const minute = minutes % 60;
const parts = [];
if (year > 0) parts.push(`${year}년`);
if (month > 0) parts.push(`${month}개월`);
if (day > 0) parts.push(`${day}일`);
if (hour > 0) parts.push(`${hour}시간`);
if (minute > 0) parts.push(`${minute}분`);
return parts.join(' ');
};
const columns = [
columnHelper.accessor((_, index) => index + 1, {
id: 'no',
header: 'NO',
}),
columnHelper.accessor('link_sn', {
id: 'link_sn',
header: '연계일련번호',
}),
columnHelper.accessor('link_nm', {
id: 'link_nm',
header: '연계 명',
}),
columnHelper.accessor('link_url', {
id: 'link_url',
header: '연계 URL',
cell: (info) => (info.getValue() ? info.getValue() : '-'),
}),
columnHelper.accessor('link_srvr_ip', {
id: 'link_srvr_ip',
header: '연계서버 IP',
cell: (info) => (info.getValue() ? info.getValue() : '-'),
}),
columnHelper.accessor('link_batch_time', {
id: 'link_batch_time',
header: '연계 주기',
cell: (info) => convertMinutesToPeriod(Number(info.getValue())),
}),
columnHelper.accessor('srvc_usg', {
id: 'srvc_usg',
header: '서비스용도',
cell: (info) => (info.getValue() ? info.getValue() : '-'),
}),
columnHelper.display({
id: 'last_link_rslt_cd',
header: '연계상태',
cell: (info) => {
const resultCd = info.row.original.last_link_rslt_cd;
return <span className={`${(resultCd === 'SUCC') ? 'text-[rgb(53,162,235)]' : (resultCd === 'FAIL' ? 'text-[rgb(255,99,132)]' : 'text-[rgba(31,208,120,1)]')}`}>{resultCd === 'SUCC' ? '성공' : (resultCd === 'FAIL' ? '실패' : '대기')}</span>
},
}),
columnHelper.accessor('last_link_excn_dt', {
id: 'last_link_excn_dt',
header: '최종연계일',
cell: (info) => (info.getValue() ? dayjs(info.getValue()).format('YYYY-MM-DD HH:mm') : '-'),
}),
columnHelper.display({
id: 'quick_start_btn',
header: '즉시 실행',
cell: (info) => {
return <QuickStartButton linkSn={info.row.original.link_sn} />
}
})
];
export { columns };
@@ -0,0 +1,36 @@
import { useMutation } from "@tanstack/react-query";
import { LinkApi } from "@/entities/link";
import { useAlertStore } from "@/shared/lib/use-alert-store";
import { Button } from "@common/components";
const QuickStartButton: React.FC<{ linkSn: string }> = ({ linkSn }) => {
const { setMessage: alert } = useAlertStore((state) => state);
const { mutateAsync: updateQuickStart } = useMutation({
mutationFn: (linkSn: string) => LinkApi.linkQuickStart(linkSn),
onSuccess: (result) => {
if (result === 'succ') {
alert('즉시 실행 요청이 완료되었습니다.');
} else {
alert('기존 요청이 실행 대기중입니다.');
}
},
onError: () => {
alert('즉시 실행 요청을 실패했습니다.');
},
});
return (
<Button
color="sky"
onClick={(e) => {
e.stopPropagation();
updateQuickStart(linkSn);
}}
>
즉시 실행
</Button>
);
};
export default QuickStartButton;
+590
View File
@@ -0,0 +1,590 @@
import {
Button,
Input,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from "@common/components";
import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import type { UseFormReturn } from "react-hook-form";
import { type LinkFormData } from "@/entities/link";
import dayjs from "dayjs";
import { useRouter } from "next/navigation";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useState } from "react";
type LinkFormProps = {
form: UseFormReturn<LinkFormData>;
linkSn?: string;
updateLinkInfo?: (data: LinkFormData) => void;
handleInsertLink?: (data: LinkFormData) => void;
deleteLink?: () => void;
updateLinkResultCode?: () => void;
}
const LinkForm = ({ form, linkSn, updateLinkInfo, handleInsertLink, deleteLink, updateLinkResultCode }: LinkFormProps) => {
const isModifyMode = !!linkSn;
const router = useRouter();
const { confirm } = useConfirm();
const [onConfirm, setOnConfirm] = useState(false);
const handleConfirm = async (callback: () => Promise<void>) => {
setOnConfirm(true);
await callback();
setOnConfirm(false);
};
const handleSubmit = async (data: LinkFormData) => {
const confirmMsg = !!linkSn ? '수정 하시겠습니까?' : '저장 하시겠습니까?';
await handleConfirm(async () => {
if (await confirm(confirmMsg)) {
if (linkSn) {
updateLinkInfo?.(data);
} else {
handleInsertLink?.(data);
}
}
});
};
const handleDelete = async () => {
await handleConfirm(async () => {
if (await confirm('삭제하시겠습니까?')) {
deleteLink?.();
}
})
};
const convertMinutesToPeriod = (minutes: number) => {
if (!minutes) return {
year: 0,
month: 0,
day: 0,
hour: 0,
minute: 0
};
const year = Math.floor(minutes / (525600)); // 365일 * 24시간 * 60분
minutes = minutes % 525600;
const month = Math.floor(minutes / (43200)); // 30일 * 24시간 * 60분
minutes = minutes % 43200;
const day = Math.floor(minutes / 1440); // 24시간 * 60분
minutes = minutes % 1440;
const hour = Math.floor(minutes / 60);
const minute = minutes % 60;
const timeParts = {
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
};
return timeParts;
};
return (
<>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)}>
<div className="card">
<Table variant="secondary">
<colgroup>
<col width="20%" />
<col width="80%" />
</colgroup>
<TableBody>
{isModifyMode &&
<TableRow>
<TableHead>연계 일련번호</TableHead>
<TableCell>
<FormField
control={form.control}
name="link_sn"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="w-[12rem] !text-[1.3rem]"
disabled={true}
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
}
<TableRow>
<TableHead>* 연계 명</TableHead>
<TableCell>
<FormField
control={form.control}
name="link_nm"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
autoComplete="off"
/>
</FormControl>
{form.formState.errors.link_nm && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.link_nm.message}
</p>
)}
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>* 연계 분류</TableHead>
<TableCell>
<FormField
control={form.control}
name="link_clsf_cd"
render={({ field }) => (
<FormItem>
<FormControl>
<Select value={field.value ?? "none"} onValueChange={(value) => field.onChange(value)}>
<SelectTrigger id="link_clsf_cd" className="w-[20rem]">
<SelectValue placeholder="전체"></SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="none">선택</SelectItem>
<SelectItem value="link0001">TIBERO 데이터베이스</SelectItem>
<SelectItem value="link0002">ORACLE 데이터베이스</SelectItem>
<SelectItem value="link0003">공공데이터 포털 API</SelectItem>
<SelectItem value="link0004">한강홍수통제소 API</SelectItem>
<SelectItem value="link0005">기상청 API</SelectItem>
<SelectItem value="link0006">국가지하수정보센터 API</SelectItem>
<SelectItem value="link0007">행정정보공동이용센터 연계</SelectItem>
</SelectContent>
</Select>
</FormControl>
{form.formState.errors.link_clsf_cd && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.link_clsf_cd.message}
</p>
)}
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>* 연계 URL</TableHead>
<TableCell>
<FormField
control={form.control}
name="link_url"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
autoComplete="off"
/>
</FormControl>
{form.formState.errors.link_url && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.link_url.message}
</p>
)}
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>연계서버 IP</TableHead>
<TableCell>
<FormField
control={form.control}
name="link_srvr_ip"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
autoComplete="off"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>* 연계 KEY</TableHead>
<TableCell>
<FormField
control={form.control}
name="link_key"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
autoComplete="off"
/>
</FormControl>
{form.formState.errors.link_key && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.link_key.message}
</p>
)}
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>연계 ID</TableHead>
<TableCell>
<FormField
control={form.control}
name="link_id"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
autoComplete="off"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>연계 비밀번호</TableHead>
<TableCell>
<FormField
control={form.control}
name="link_pswd"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
type="password"
value={field.value ?? ""}
className="!text-[1.3rem]"
autoComplete="off"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>서비스 용도</TableHead>
<TableCell>
<FormField
control={form.control}
name="srvc_usg"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
autoComplete="off"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>연계 주기</TableHead>
<TableCell>
<FormField
control={form.control}
name="link_batch_time"
render={({ field }) => {
const timeParts = convertMinutesToPeriod(Number(field.value));
return (
<FormItem>
<FormControl>
<div className="flex items-center gap-6">
<div className="flex items-center">
<Input
value={timeParts.year}
className="w-20 !text-[1.3rem]"
type="number"
min={0}
onChange={(e) => {
const total = (Number(e.target.value) * 525600) +
(timeParts.month * 43200) +
(timeParts.day * 1440) +
(timeParts.hour * 60) +
timeParts.minute;
field.onChange(total);
}}
/>
<span className="ml-2">년</span>
</div>
<div className="flex items-center">
<Input
value={timeParts.month}
className="w-20 !text-[1.3rem]"
type="number"
min={0}
max={11}
onChange={(e) => {
const total = (timeParts.year * 525600) +
(Number(e.target.value) * 43200) +
(timeParts.day * 1440) +
(timeParts.hour * 60) +
timeParts.minute;
field.onChange(total);
}}
/>
<span className="ml-2">월</span>
</div>
<div className="flex items-center">
<Input
value={timeParts.day}
className="w-20 !text-[1.3rem]"
type="number"
min={0}
max={30}
onChange={(e) => {
const total = (timeParts.year * 525600) +
(timeParts.month * 43200) +
(Number(e.target.value) * 1440) +
(timeParts.hour * 60) +
timeParts.minute;
field.onChange(total);
}}
/>
<span className="ml-2">일</span>
</div>
<div className="flex items-center">
<Input
value={timeParts.hour}
className="w-20 !text-[1.3rem]"
type="number"
min={0}
max={23}
onChange={(e) => {
const total = (timeParts.year * 525600) +
(timeParts.month * 43200) +
(timeParts.day * 1440) +
(Number(e.target.value) * 60) +
timeParts.minute;
field.onChange(total);
}}
/>
<span className="ml-2">시간</span>
</div>
<div className="flex items-center">
<Input
value={timeParts.minute}
className="w-20 !text-[1.3rem]"
type="number"
min={0}
max={59}
onChange={(e) => {
const total = (timeParts.year * 525600) +
(timeParts.month * 43200) +
(timeParts.day * 1440) +
(timeParts.hour * 60) +
Number(e.target.value);
field.onChange(total);
}}
/>
<span className="ml-2">분</span>
</div>
</div>
</FormControl>
{form.formState.errors.link_batch_time && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.link_batch_time.message}
</p>
)}
</FormItem>
);
}}
/>
</TableCell>
</TableRow>
{isModifyMode &&
<>
<TableRow>
<TableHead>연계 상태</TableHead>
<TableCell>
<FormField
control={form.control}
name="last_link_rslt_cd"
render={({ field }) => {
return (
<FormItem>
<FormControl>
<div className="flex items-center gap-6">
<Input
{...field}
value={field.value ? (field.value === 'SUCC' ? '성공' : (field.value === 'FAIL' ? '실패' : '대기')) : ''}
className="!text-[1.3rem] w-[15rem]"
disabled
/>
<Button color="green" type="button" onClick={() => updateLinkResultCode?.()}>대기로 전환</Button>
</div>
</FormControl>
</FormItem>
);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>최종 연계 실행일</TableHead>
<TableCell>
<FormField
control={form.control}
name="last_link_excn_dt"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ? dayjs(field.value).format('YYYY-MM-DD HH:mm:ss') : ""}
className="!text-[1.3rem]"
disabled
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>다음 연계 실행일</TableHead>
<TableCell>
<FormField
control={form.control}
name="next_link_excn_dt"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ? dayjs(field.value).format('YYYY-MM-DD HH:mm:ss') : ""}
className="!text-[1.3rem]"
disabled
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>수정자</TableHead>
<TableCell>
<FormField
control={form.control}
name="mdfr"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
disabled
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>수정일시</TableHead>
<TableCell>
<FormField
control={form.control}
name="mdfcn_dt"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ? dayjs(field.value).format('YYYY-MM-DD') : ""}
className="!text-[1.3rem]"
disabled
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
</>
}
</TableBody>
</Table>
</div>
<div className="flex justify-center gap-4 pb-12 pt-10">
<Button
size="lg"
color="white"
type="button"
onClick={() => router.back()}
>
목록
</Button>
<Button size="lg" type="submit">
저장
</Button>
{isModifyMode && (
<Button size="lg" color="red" type="button" onClick={handleDelete}>
삭제
</Button>
)}
</div>
</form>
</Form>
<ConfirmDialog isOpen={onConfirm} />
</>
)
};
export default LinkForm;
+134
View File
@@ -0,0 +1,134 @@
"use client";
import "@common/assets/styles/grid.css";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Button, Input, Label, LoadingWithAnimation } from "@common/components";
import { getCoreRowModel } from "@tanstack/react-table";
import { LinkApi, type Link, type LinkSearchParam } from "@/entities/link";
import { useQuery } from "@tanstack/react-query";
import { columns } from "../model/table-columns";
import { useQueryClient } from "@tanstack/react-query";
import { useDataTable } from "@/shared/lib";
function LinkPage() {
const router = useRouter();
const queryClient = useQueryClient();
const [linkSn, setLinkSn] = useState<string>('');
const [linkNm, setLinkNm] = useState<string>('');
const [linkSearchParam, setLinkSearchParam] = useState<LinkSearchParam>({
linkSn: '',
linkNm: ''
});
const query = useQuery({
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, linkSearchParam],
queryFn: () => LinkApi.getLinkList(linkSearchParam),
});
const handleResetSearchParam = () => {
setLinkSn('');
setLinkNm('');
setLinkSearchParam({
...linkSearchParam,
linkSn: '',
linkNm: '',
});
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLinkSearchParam({
...linkSearchParam,
linkSn,
linkNm
});
};
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,
},
});
return (
<div className="card card-border !mt-6">
<div className="card card-border">
<form className="flex items-center gap-2 p-2" onSubmit={handleSubmit}>
<Label htmlFor="liSnSearchInput" className="mr-2 text-label">
일련번호
</Label>
<Input
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-4"
id="liSnSearchInput"
value={linkSn}
onChange={(e) => setLinkSn(e.target.value)}
autoComplete="off"
/>
<Label htmlFor="liNmSearchInput" className="mr-2 text-label">
연계 명
</Label>
<Input
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-2"
id="liNmSearchInput"
value={linkNm}
onChange={(e) => setLinkNm(e.target.value)}
autoComplete="off"
/>
<Button color="green" className="h-[3.2rem]" type="button" onClick={handleResetSearchParam}>
초기화
</Button>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
</form>
</div>
<div className="card card-border flex flex-col h-[calc(100vh-28rem)]">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">연계 목록</h3>
<div>
<Button onClick={handleClickRegist}>등록</Button>
</div>
</div>
<div className="card !m-[1.2rem] overflow-auto h-full !p-0">
{!query.isLoading ?
<div>{tableComponent}</div>
:
<div className="[data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center h-full">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
}
</div>
</div>
</div>
);
}
export { LinkPage };
+111
View File
@@ -0,0 +1,111 @@
"use client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { LinkApi, linkFormSchema, type LinkFormData } from "@/entities/link";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import LinkForm from "./link-form";
import { useAlertStore } from "@/shared/lib/use-alert-store";
import { useEffect } from "react";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import { AUTH_USER_ID } from "@/shared/config";
import { getCookie } from "@common/components";
function ModifyLinkPage({ linkSn }: { linkSn: string }) {
const router = useRouter();
const queryClient = useQueryClient();
const { setMessage: alert } = useAlertStore((state) => state);
const userUniqId = getCookie(AUTH_USER_ID) ?? "";
// 컴포넌트 마운트 시 캐시 초기화
useEffect(() => {
queryClient.removeQueries({
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkInfo`],
});
}, [queryClient]);
const { data: linkInfo, refetch } = useQuery({
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkInfo`],
queryFn: () => LinkApi.getLinkInfo(linkSn),
enabled: !!linkSn,
});
const { mutateAsync: updateLinkInfo } = useMutation({
mutationFn: (data: LinkFormData) => LinkApi.updateLinkInfo({
...data,
link_clsf_cd: data.link_clsf_cd !== 'none' ? data.link_clsf_cd : null,
mdfr: userUniqId
}),
onSuccess: () => {
alert('수정되었습니다.');
router.push('/link');
}
});
const { mutateAsync: deleteLink } = useMutation({
mutationFn: () => LinkApi.deleteLink(linkSn),
onSuccess: () => {
alert('삭제되었습니다.', router.back);
}
});
const { mutateAsync: updateLinkResultCode } = useMutation({
mutationFn: () => LinkApi.updateLinkResultCode(linkSn),
onSuccess: () => {
alert('대기로 전환되었습니다.');
refetch();
}
});
const fetchUserInfo = async () => {
const userData = await getUserInfo();
return userData.userName;
};
const form = useForm<LinkFormData>({
resolver: zodResolver(linkFormSchema),
defaultValues: {},
});
useEffect(() => {
const applyFormData = async () => {
if (linkInfo) {
let newData = { ...linkInfo };
if (linkInfo.mdfr) {
const userName = await fetchUserInfo();
newData = {
...linkInfo,
mdfr: userName,
};
}
form.reset(newData);
}
};
applyFormData();
}, [linkInfo]);
return (
<>
<div className="card card-border !mb-0 !mt-6">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">연계정보 수정</h3>
</div>
<LinkForm
form={form}
linkSn={linkSn}
updateLinkInfo={updateLinkInfo}
deleteLink={deleteLink}
updateLinkResultCode={updateLinkResultCode}
/>
</div>
</>
);
};
export { ModifyLinkPage };
+64
View File
@@ -0,0 +1,64 @@
"use client";
import { LinkApi, type LinkFormData, linkFormSchema } from "@/entities/link";
import LinkForm from "./link-form";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import dayjs from "dayjs";
import { useMutation } from "@tanstack/react-query";
import { useAlertStore } from "@/shared/lib/use-alert-store";
import { useRouter } from "next/navigation";
import { AUTH_USER_ID } from "@/shared/config";
import { getCookie } from "@common/components";
function RegistLinkPage() {
const { setMessage: alert } = useAlertStore((state) => state);
const router = useRouter();
const userUniqId = getCookie(AUTH_USER_ID) ?? "";
const form = useForm<LinkFormData>({
resolver: zodResolver(linkFormSchema),
defaultValues: {
link_nm: '',
link_url: '',
link_srvr_ip: '',
link_db_cd: '',
link_key: '',
link_id: '',
link_pswd: '',
srvc_usg: '',
link_batch_time: 5,
link_clsf_cd: 'none',
},
});
const { mutate: insertLinkInfo } = useMutation({
mutationFn: (data: LinkFormData) => LinkApi.insertLinkInfo(data),
onSuccess: () => {
alert('저장되었습니다.');
router.push('/link');
}
});
const handleInsertLink = (data: LinkFormData) => {
const insertData = {
...data,
link_clsf_cd: data.link_clsf_cd !== 'none' ? data.link_clsf_cd : null,
next_link_excn_dt: dayjs().add(Number(data.link_batch_time), 'minute').format('YYYY-MM-DD HH:mm:ss'),
mdfr: userUniqId,
};
insertLinkInfo(insertData);
};
return (
<div className="card card-border !mb-0 !mt-6">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">연계 등록</h3>
</div>
<LinkForm form={form} handleInsertLink={handleInsertLink} />
</div>
);
};
export { RegistLinkPage };
+1
View File
@@ -0,0 +1 @@
export { LinkLogPage } from './ui/link-log-page';
@@ -0,0 +1,55 @@
import dayjs from "dayjs";
import { createColumnHelper } from "@tanstack/react-table";
import { type LinkLog } from "@/entities/link";
const columnHelper = createColumnHelper<LinkLog>();
const columns = [
columnHelper.accessor('rownum', {
id: 'rownum',
header: 'NO',
}),
columnHelper.accessor('link_sn', {
id: 'link_sn',
header: '연계일련번호',
}),
columnHelper.accessor('link_nm', {
id: 'link_nm',
header: '연계 명',
}),
columnHelper.display({
id: 'link_rslt_cd',
header: '연계실행결과',
cell: (info) => {
const isSucc = info.row.original.link_rslt_cd.trim() === 'SUCC';
return <span className={`${isSucc ? 'text-[rgb(53,162,235)]' : 'text-[rgb(255,99,132)]'}`}>{isSucc ? '성공' : '실패'}</span>
},
}),
columnHelper.accessor('link_err_cd', {
id: 'link_err_cd',
header: '연계오류코드',
cell: (info) => (info.getValue() ? info.getValue() : '-'),
}),
columnHelper.accessor('err_cn', {
id: 'err_cn',
header: '연계오류메세지',
cell: (info) => (
<div
className="max-w-[500px] truncate"
title={info.getValue() ?? ''}
>
{info.getValue() || '-'}
</div>
),
}),
columnHelper.accessor('log_crt_dt', {
id: 'log_crt_dt',
header: '연계실행일자',
cell: (info) => {
const date = dayjs(info.getValue());
return date.format('YYYY-MM-DD HH:mm');
}
}),
];
export { columns };
@@ -0,0 +1,189 @@
"use client";
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 { useQuery } from "@tanstack/react-query";
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";
import { useAlertStore } from "@/shared/lib/use-alert-store";
import { useDataTable } from "@/shared/lib";
function LinkLogPage() {
const { setMessage: alert } = useAlertStore((state) => state);
const [linkSn, setLinkSn] = useState<string>('');
const [linkNm, setLinkNm] = useState<string>('');
const [isSucc, setIsSucc] = useState<string>('ALL');
const [startDate, setStartDate] = useState<string>(dayjs().subtract(7, "day").format('YYYY-MM-DD'));
const [endDate, setEndDate] = useState<string>(dayjs().format('YYYY-MM-DD'));
const [linkSearchParam, setLinkSearchParam] = useState<LinkSearchParam>({
linkSn: '',
linkNm: '',
isSucc: '',
startDate: '',
endDate: '',
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 100,
});
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const query = useQuery({
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, linkSearchParam, pagination],
queryFn: () =>
LinkApi.getLinkLogList({
...linkSearchParam,
page: pagination.pageIndex,
size: pagination.pageSize,
isSucc,
startDate,
endDate,
}),
});
const { tableComponent } = useDataTable({
query,
tableOptions: {
columns,
state: {
pagination,
rowSelection,
},
manualPagination: true,
enableMultiRowSelection: true,
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
onRowSelectionChange: setRowSelection,
},
dataTableOptions: {
viewPagination: true,
},
});
const handleResetSearchParam = () => {
setIsSucc('ALL');
setLinkNm('');
setLinkSn('');
setStartDate(dayjs().subtract(7, "day").format('YYYY-MM-DD'));
setEndDate(dayjs().format('YYYY-MM-DD'));
setLinkSearchParam({
...linkSearchParam,
linkSn: '',
linkNm: '',
isSucc: 'ALL',
startDate: dayjs().subtract(7, "day").format('YYYY-MM-DD'),
endDate: dayjs().format('YYYY-MM-DD'),
});
};
const handleSubmit = async (e?: React.FormEvent<HTMLFormElement>) => {
e?.preventDefault();
if (startDate && endDate) {
if (startDate > endDate) {
alert('종료일이 시작일보다 이전일 수 없습니다.');
return;
} else {
setPagination({
...pagination,
pageIndex: 0,
});
setLinkSearchParam({
...linkSearchParam,
linkSn,
linkNm,
isSucc,
startDate,
endDate,
});
}
} else {
alert('조회 날짜를 선택해주세요.');
return;
}
};
return (
<div className="card card-border !mt-6">
<div className="card card-border">
<form className="flex items-center gap-2 p-2" onSubmit={handleSubmit}>
<Label htmlFor="liSnSearchInput" className="mr-2 text-label">
일련번호
</Label>
<Input
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-4"
id="liSnSearchInput"
value={linkSn}
onChange={(e) => setLinkSn(e.target.value)}
autoComplete="off"
/>
<Label htmlFor="liNmSearchInput" className="mr-2 text-label">
연계 명
</Label>
<Input
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-4"
id="liNmSearchInput"
value={linkNm}
onChange={(e) => setLinkNm(e.target.value)}
autoComplete="off"
/>
<Label htmlFor="liResultCode" className="mr-2 text-label">
실행결과
</Label>
<Select onValueChange={(e) => setIsSucc(e)} value={isSucc}>
<SelectTrigger id="liResultCode" className="w-[10rem] mr-4">
<SelectValue placeholder="전체"></SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">전체</SelectItem>
<SelectItem value="SUCC">성공</SelectItem>
<SelectItem value="FAIL">실패</SelectItem>
</SelectContent>
</Select>
<Label htmlFor="sDateSearchInput" className="mx-2 text-label">
시작일
</Label>
<DatePicker id="sDateSearchInput" mode={"single"} selected={dayjs(startDate).toDate()} onSelect={(e) => setStartDate(dayjs(e).format('YYYY-MM-DD'))} />
<Label htmlFor="eDateSearchInput" className="mx-2 text-label">
종료일
</Label>
<DatePicker id="eDateSearchInput" mode={"single"} className="mr-2" selected={dayjs(endDate).toDate()} onSelect={(e) => setEndDate(dayjs(e).format('YYYY-MM-DD'))} />
<Button color="green" className="h-[3.2rem]" type="button" onClick={handleResetSearchParam}>
초기화
</Button>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
</form>
</div>
<div className="card card-border flex flex-col h-[calc(100vh-28rem)]">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">로그 목록 (검색 조건에 따른 건수 : {query.data?.totalElements.toLocaleString('ko-KR') ?? 0} )</h3>
</div>
<div className="card !m-[1.2rem] overflow-auto h-full !p-0">
{query.isLoading ?
<div className="[data-theme='black']:bg-transparent rounded-lg p-10 h-full flex justify-center items-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
:
<div>{tableComponent}</div>
}
</div>
</div>
</div>
)
};
export { LinkLogPage };
+1
View File
@@ -0,0 +1 @@
export { LinkParamsPage } from './ui/link-params-page';
@@ -0,0 +1,26 @@
import { createColumnHelper } from "@tanstack/react-table";
import { type Link } from "@/entities/link";
const columnHelper = createColumnHelper<Link>();
const columns = [
columnHelper.accessor((_, index) => index + 1, {
id: 'no',
header: 'NO',
}),
columnHelper.accessor('link_sn', {
id: 'link_sn',
header: '연계 일련번호',
}),
columnHelper.accessor('link_nm', {
id: 'link_nm',
header: '연계 명',
}),
columnHelper.accessor('srvc_usg', {
id: 'srvc_usg',
header: '서비스용도',
cell: (info) => (info.getValue() ? info.getValue() : '-'),
}),
];
export { columns };
@@ -0,0 +1,98 @@
import { createColumnHelper } from "@tanstack/react-table";
import { type LinkVrbl } from "@/entities/link";
import { Checkbox } from "@common/components";
const columnHelper = createColumnHelper<LinkVrbl>();
function vrblColumns({
selectedVrblIds,
setSelectedVrblIds,
}: {
selectedVrblIds: string[];
setSelectedVrblIds: React.Dispatch<React.SetStateAction<string[]>>;
}) {
return [
columnHelper.display({
id: "select",
header: ({ table }) => {
const allRows = table.getRowModel().rows;
const allSelected =
allRows.length > 0 &&
allRows.every((row) =>
selectedVrblIds.includes(row.original.link_vrbl_sn)
);
return (
<div className="flex justify-center items-center">
<Checkbox
className="bg-white"
checked={
allSelected
? true
: false
}
onCheckedChange={(checked) => {
if (checked) {
setSelectedVrblIds(allRows.map((row) => row.original.link_vrbl_sn));
} else {
setSelectedVrblIds([]);
}
}}
/>
</div>
);
},
cell: ({ row }) => {
const id = row.original.link_vrbl_sn;
const checked = selectedVrblIds.includes(id);
return (
<div className="flex justify-center items-center">
<Checkbox
checked={checked}
onClick={(e) => e.stopPropagation()}
onCheckedChange={(checked) => {
setSelectedVrblIds((prev) =>
checked ? [...prev, id] : prev.filter((x) => x !== id)
);
}}
/>
</div>
);
},
}),
columnHelper.accessor("link_vrbl_sn", {
id: "link_vrbl_sn",
header: "연계 변수 일련번호",
}),
columnHelper.accessor("link_vrbl_nm", {
id: "link_vrbl_nm",
header: "변수 명",
}),
columnHelper.accessor("link_vrbl_cd", {
id: "link_vrbl_cd",
header: "변수코드",
cell: (info) => (info.getValue() ? info.getValue() : '-'),
}),
columnHelper.accessor("link_vrbl_vl", {
id: "link_vrbl_vl",
header: "변수 값",
cell: (info) => (info.getValue() === 'now' ? '요청일자' : info.getValue() === 'nowMonth' ? '요청월' : info.getValue()),
}),
columnHelper.accessor("link_vrbl_expln", {
id: "link_vrbl_expln",
header: "설명",
cell: (info) => (
<div
className="max-w-72 truncate"
title={info.getValue() ?? ''}
>
{info.getValue() || '-'}
</div>
),
}),
];
}
export { vrblColumns };
@@ -0,0 +1,351 @@
'use client';
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogClose,
Button,
Input,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Checkbox,
RadioGroup,
RadioGroupItem,
Label
} from "@common/components";
import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form";
import { type LinkVrblFormData } from "@/entities/link";
import dayjs from "dayjs";
import type { UseFormReturn } from "react-hook-form";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useState, useEffect } from "react";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
type LinkParamsDialogProps = {
selectedLink: {
linkSn: string;
linkNm: string;
};
form: UseFormReturn<LinkVrblFormData>;
insertLinkVrbl: (data: LinkVrblFormData) => void;
dialogOpen: boolean;
onOpenChange: (val: boolean) => void;
updateLinkVrbl: (data: LinkVrblFormData) => void;
}
function LinkParamsDialog({ selectedLink, form, insertLinkVrbl, dialogOpen, onOpenChange, updateLinkVrbl }: LinkParamsDialogProps) {
const isModifyMode = !!form.getValues('link_vrbl_sn');
const { confirm } = useConfirm();
const [onConfirm, setOnConfirm] = useState<boolean>(false);
const [isToday, setIsToday] = useState<boolean>(false);
const [checkNowMonth, setCheckNowMonth] = useState<string>('now');
const handleConfirm = async (callback: () => Promise<void>) => {
setOnConfirm(true);
await callback();
setOnConfirm(false);
};
function replaceEmptyWithNull<T extends Record<string, unknown>>(data: T): T {
const result = {} as T;
for (const [key, value] of Object.entries(data)) {
(result as Record<string, unknown>)[key] = value === '' ? null : value;
}
return result as T;
};
const handleSubmit = async (data: LinkVrblFormData) => {
const confirmMsg = isModifyMode ? '수정 하시겠습니까?' : '저장 하시겠습니까?';
await handleConfirm(async () => {
const cleanData = replaceEmptyWithNull(data);
if (await confirm(confirmMsg)) {
if (isModifyMode) {
updateLinkVrbl(isToday ? { ...cleanData, link_vrbl_vl: checkNowMonth } : cleanData);
} else {
insertLinkVrbl(isToday ? { ...cleanData, link_vrbl_vl: checkNowMonth } : cleanData);
}
}
});
};
const handleCheckTodayValue = (check: boolean | string) => {
if (check) {
setIsToday(true);
form.setValue('link_vrbl_vl', (checkNowMonth === 'now' ? '요청일자' : '요청월'));
} else {
setIsToday(false);
form.setValue('link_vrbl_vl', '');
}
};
useEffect(() => {
if (!dialogOpen) {
setIsToday(false);
setCheckNowMonth('now');
form.reset({
link_vrbl_nm: '',
link_vrbl_expln: '',
link_vrbl_cd: '',
link_vrbl_vl: '',
});
}
if (isModifyMode && (form.getValues('link_vrbl_vl') === 'now' || form.getValues('link_vrbl_vl') === 'nowMonth')) {
setIsToday(true);
setCheckNowMonth(form.getValues('link_vrbl_vl'));
}
}, [dialogOpen]);
return (
<Dialog
open={dialogOpen}
onOpenChange={(open) => {
onOpenChange(open);
}}
>
<DialogTrigger asChild>
<Button color="green" className="h-[3.2rem]" onClick={() => onOpenChange(true)}>행 추가</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[55rem]">
<DialogHeader>
<DialogTitle>
{selectedLink.linkNm} ({selectedLink.linkSn})
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)}>
<div className="card">
<Table variant="secondary">
<colgroup>
<col width="30%" />
<col width="70%" />
</colgroup>
<TableBody>
{isModifyMode &&
<TableRow>
<TableHead>변수 일련번호</TableHead>
<TableCell>
<FormField
control={form.control}
name="link_vrbl_sn"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
disabled
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
}
<TableRow>
<TableHead>* 변수 명</TableHead>
<TableCell>
<FormField
control={form.control}
name="link_vrbl_nm"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
autoComplete="off"
/>
</FormControl>
{form.formState.errors.link_vrbl_nm && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.link_vrbl_nm.message}
</p>
)}
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>변수 설명</TableHead>
<TableCell>
<FormField
control={form.control}
name="link_vrbl_expln"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
autoComplete="off"
/>
</FormControl>
{form.formState.errors.link_vrbl_expln && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.link_vrbl_expln.message}
</p>
)}
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>변수 코드</TableHead>
<TableCell>
<FormField
control={form.control}
name="link_vrbl_cd"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
autoComplete="off"
/>
</FormControl>
{form.formState.errors.link_vrbl_cd && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.link_vrbl_cd.message}
</p>
)}
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>* 변수 값</TableHead>
<TableCell>
<FormField
control={form.control}
name="link_vrbl_vl"
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-x-2 mb-4">
<Checkbox id="isToday" className="!border-gray-600" checked={isToday} onCheckedChange={(check) => handleCheckTodayValue(check)} />
<label htmlFor="isToday" className="text-gray-600">변수 값이 오늘 날짜 일시 체크해 주세요.</label>
</div>
{isToday &&
<div className="flex items-center gap-x-2 mb-4">
<RadioGroup
name="checkNowMonth"
onValueChange={(value) => {
setCheckNowMonth(value);
form.setValue('link_vrbl_vl', (value === 'now' ? '요청일자' : '요청월'));
}}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="now" id="now" checked={checkNowMonth === 'now'} />
<Label htmlFor="now">요청일자(YYYY-MM-DD)</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="nowMonth" id="nowMonth" checked={checkNowMonth === 'nowMonth'} />
<Label htmlFor="nowMonth">요청월(YYYY-MM)</Label>
</div>
</RadioGroup>
</div>
}
<FormControl>
<Input
{...field}
value={field.value ? (field.value === 'now' ? '요청일자' : field.value === 'nowMonth' ? '요청월' : field.value) : ''}
className={`!text-[1.3rem] ${isToday ? 'bg-gray-100' : ''}`}
autoComplete="off"
disabled={isToday}
/>
</FormControl>
{form.formState.errors.link_vrbl_vl && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.link_vrbl_vl.message}
</p>
)}
</FormItem>
)}
/>
</TableCell>
</TableRow>
{isModifyMode &&
<>
<TableRow>
<TableHead>수정자</TableHead>
<TableCell>
<FormField
control={form.control}
name="mdfr"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
disabled
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>수정일시</TableHead>
<TableCell>
<FormField
control={form.control}
name="mdfcn_dt"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ? dayjs(field.value).format('YYYY-MM-DD') : ""}
className="!text-[1.3rem]"
disabled
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
</>
}
</TableBody>
</Table>
</div>
<DialogFooter>
<Button type="submit">저장</Button>
<DialogClose asChild>
<Button color="white">취소</Button>
</DialogClose>
</DialogFooter>
</form>
</Form>
<ConfirmDialog isOpen={onConfirm} />
</DialogContent>
</Dialog>
)
};
export { LinkParamsDialog };
@@ -0,0 +1,405 @@
'use client';
import { useState, useMemo } from "react";
import {
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 { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { columns } from "../model/table-columns";
import { vrblColumns } from "../model/vrbl-table-columns";
import { type LinkVrbl } from "@/entities/link";
import { useAlertStore } from "@/shared/lib/use-alert-store";
import { LinkParamsDialog } from "./link-params-dialog";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import { AUTH_USER_ID } from "@/shared/config";
import { getCookie } from "@common/components";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { useDataTable } from "@/shared/lib";
function LinkParamsPage() {
const queryClient = useQueryClient();
const [linkSn, setLinkSn] = useState<string>('');
const [linkNm, setLinkNm] = useState<string>('');
const [linkSearchParam, setLinkSearchParam] = useState<LinkSearchParam>({
linkSn: '',
linkNm: ''
});
const [selectedLink, setSelectedLink] = useState<{
linkSn: string;
linkNm: string;
}>({
linkSn: '',
linkNm: ''
});
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 100,
});
const { confirm } = useConfirm();
const [selectedVrblIds, setSelectedVrblIds] = useState<string[]>([]);
const [isDBdata, setIsDBData] = useState<boolean>(false);
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const [onConfirm, setOnConfirm] = useState<boolean>(false);
const userUniqId = getCookie(AUTH_USER_ID) ?? "";
const { setMessage: alert } = useAlertStore((state) => state);
const dbIdList = ['link0001', 'link0002'];
// Link 목록 쿼리
const query = useQuery({
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/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: ''
});
setLinkSn('');
setLinkNm('');
setLinkSearchParam({
...linkSearchParam,
linkSn: '',
linkNm: ''
});
};
const handleConfirm = async (callback: () => Promise<void>) => {
setOnConfirm(true);
await callback();
setOnConfirm(false);
};
const { mutateAsync: insertLinkVrbl } = useMutation({
mutationFn: async (data: LinkVrblFormData) =>
await LinkApi.insertLinkVrbl({
...data,
link_sn: selectedLink.linkSn,
mdfr: 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: 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.invalidateQueries({
queryKey: ['linkVrblList', selectedLink.linkSn]
});
setSelectedVrblIds([]);
alert('삭제되었습니다.');
}
});
// linkVrblQuery의 데이터를 사용
const tableData = useMemo(() => {
return linkVrblQuery.data?.content ?? [];
}, [linkVrblQuery.data]);
const vrblCols = useMemo(
() =>
vrblColumns({
selectedVrblIds,
setSelectedVrblIds,
}),
[selectedVrblIds]
);
const vrblTable = useReactTable({
data: tableData,
columns: vrblCols,
getCoreRowModel: getCoreRowModel(),
});
const form = useForm<LinkVrblFormData>({
resolver: zodResolver(linkVrblFormSchema),
defaultValues: {},
});
const handleRowClick = async (link: Link) => {
setSelectedLink({
linkSn: link.link_sn,
linkNm: link.link_nm,
});
setSelectedVrblIds([]);
if (dbIdList.includes(link.link_clsf_cd)) {
setIsDBData(true);
} else {
setIsDBData(false);
}
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setSelectedLink({
linkSn: '',
linkNm: ''
});
setLinkSearchParam({
...linkSearchParam,
linkSn,
linkNm
});
};
const fetchUserInfo = async () => {
const userData = await getUserInfo();
return userData.userName;
};
const handleDeleteParam = async () => {
if (selectedVrblIds.length === 0) {
alert('삭제할 파라미터를 선택해 주세요.');
} else {
await handleConfirm(async () => {
if (await confirm('선택한 파라미터 정보를 삭제하시겠습니까?')) {
deleteLinkVrbl(selectedVrblIds);
}
});
}
};
const handleVrblRowClick = async (data: LinkVrbl) => {
const linkVrblInfo = await LinkApi.getLinkVrblInfo(data.link_vrbl_sn);
if (linkVrblInfo) {
let newData = { ...linkVrblInfo };
if (linkVrblInfo.mdfr) {
const userName = await fetchUserInfo();
newData = {
...linkVrblInfo,
mdfr: userName,
};
}
form.reset(newData);
}
setDialogOpen(true);
};
const { tableComponent } = useDataTable({
query,
tableOptions: {
columns,
state: {
pagination,
rowSelection,
},
manualPagination: true,
enableMultiRowSelection: true,
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
onRowSelectionChange: setRowSelection,
},
dataTableOptions: {
onRowClick: handleRowClick,
viewPagination: true,
},
});
// LinkVrbl 데이터 표시 로직
const renderLinkVrblContent = () => {
if (isDBdata) {
return (
<div className="flex items-center justify-center mt-14 text-gray-500 text-3xl flex-1">
변수를 지원하지 않는 연계 데이터입니다.
</div>
);
}
if (!selectedLink.linkSn) {
return (
<div className="flex items-center justify-center mt-14 text-gray-500 text-3xl flex-1">
연계 데이터를 선택해주세요.
</div>
);
}
if (linkVrblQuery.isLoading) {
return (
<div className="[data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center h-full">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
);
}
return (
<>
<div className="mb-2 mt-14 flex justify-end items-center flex-none">
{tableData.length > 0 && (
<Button color="red" className="h-[3.2rem] mr-2" type="button" onClick={handleDeleteParam}>
행 삭제
</Button>
)}
<LinkParamsDialog
selectedLink={selectedLink}
form={form}
insertLinkVrbl={insertLinkVrbl}
dialogOpen={dialogOpen}
onOpenChange={setDialogOpen}
updateLinkVrbl={updateLinkVrbl}
/>
</div>
{tableData.length > 0 ? (
<div className="overflow-auto flex-1 min-h-[300px]">
<DataTable table={vrblTable} onRowClick={handleVrblRowClick} />
</div>
) : (
<div className="flex items-center justify-center h-20 text-gray-500 text-3xl">
등록된 연계 변수가 없습니다.
</div>
)}
</>
);
};
return (
<div className="card card-border !mt-6">
<div className="card card-border">
<form className="flex items-center gap-2 p-2" onSubmit={handleSubmit}>
<Label htmlFor="liSnSearchInput" className="mr-2 text-label">
일련번호
</Label>
<Input
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-4"
id="liSnSearchInput"
value={linkSn}
onChange={(e) => setLinkSn(e.target.value)}
autoComplete="off"
/>
<Label htmlFor="liNmSearchInput" className="mr-2 text-label">
연계 명
</Label>
<Input
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-2"
id="liNmSearchInput"
value={linkNm}
onChange={(e) => setLinkNm(e.target.value)}
autoComplete="off"
/>
<Button color="green" className="h-[3.2rem]" type="button" onClick={handleResetSearchParam}>
초기화
</Button>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
</form>
</div>
<div className="card !mb-0 flex h-[calc(100vh-28rem)] gap-5 !p-0">
<div className="card card-border w-1/2 !mx-0 flex flex-col">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">연계 목록</h3>
</div>
<div className="card !m-[1.2rem] overflow-auto !p-0 h-full">
{!query.isLoading ? (
<div>{tableComponent}</div>
) : (
<div className="[data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center h-full">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
</div>
</div>
<div className="card card-border w-1/2 !mx-0 flex flex-col">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">연계 변수 정보</h3>
</div>
<div className="card !m-[1.2rem] !p-0 overflow-auto h-full flex flex-col">
<Table variant="secondary" className="flex-none">
<colgroup>
<col width="20%" />
<col width="80%" />
</colgroup>
<TableBody>
<TableRow>
<TableHead>연계 일련번호</TableHead>
<TableCell>
{selectedLink.linkSn ? selectedLink.linkSn : '-'}
</TableCell>
</TableRow>
<TableRow>
<TableHead>연계 명</TableHead>
<TableCell>
{selectedLink.linkNm ? selectedLink.linkNm : '-'}
</TableCell>
</TableRow>
</TableBody>
</Table>
{renderLinkVrblContent()}
</div>
</div>
</div>
<ConfirmDialog isOpen={onConfirm} />
</div>
);
}
export { LinkParamsPage };
+1
View File
@@ -0,0 +1 @@
export { LinkStatsPage } from './ui/link-stats-page';
@@ -0,0 +1,378 @@
"use client";
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,
} from "chart.js";
import { Line } from "react-chartjs-2";
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;
}
function LinkStatsPage() {
const succChartRef = useRef<Chart<"line"> | null>(null);
const failChartRef = useRef<Chart<"line"> | null>(null);
const [startDate, setStartDate] = useState<string>(dayjs().subtract(7, "day").format('YYYY-MM-DD'));
const [endDate, setEndDate] = useState<string>(dayjs().format('YYYY-MM-DD'));
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 100,
});
const [linkStatsList, setLinkStatsList] = useState<LinkStatsListObject | null>(null);
const [logQueryParam, setLogQueryParam] = useState<{
isSucc: 'SUCC' | 'FAIL';
linkSn?: string;
linkNm?: string;
} | null>(null);
const query = useQuery({
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, logQueryParam, pagination],
queryFn: () =>
LinkApi.getLinkLogList({
...logQueryParam,
page: pagination.pageIndex,
size: pagination.pageSize,
startDate,
endDate,
}),
enabled: !!logQueryParam,
});
const { tableComponent } = useDataTable({
query,
tableOptions: {
columns,
state: {
pagination,
rowSelection,
},
manualPagination: true,
enableMultiRowSelection: true,
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
onRowSelectionChange: setRowSelection,
},
dataTableOptions: {
viewPagination: true,
},
});
const [linkSearchParam, setLinkSearchParam] = useState<LinkSearchParam>({
linkSn: '',
linkNm: '',
startDate: '',
endDate: '',
});
ChartJS.register(
LineElement,
CategoryScale,
LinearScale,
PointElement,
Tooltip,
Legend
);
const succData = {
labels: linkStatsList?.linkSuccStats?.map((e) => e.link_nm),
datasets: [
{
label: '연계 성공',
data: linkStatsList?.linkSuccStats?.map((e) => e.count),
fill: false,
borderColor: 'rgb(53, 162, 235)',
backgroundColor: 'rgb(53, 162, 235)',
tension: 0.1,
pointRadius: 6,
pointHoverRadius: 8,
},
],
};
const succOptions: ChartOptions<'line'> = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
},
},
scales: {
y: {
min: 0,
},
},
};
const failData = {
labels: linkStatsList?.linkFailStats?.map((e) => e.link_nm),
datasets: [
{
label: '연계 실패',
data: linkStatsList?.linkFailStats?.map((e) => e.count),
fill: false,
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgb(255, 99, 132)',
tension: 0.1,
pointRadius: 6,
pointHoverRadius: 8,
},
],
};
const failOptions: ChartOptions<'line'> = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
},
},
scales: {
y: {
min: 0,
},
},
};
const handleResetSearchParam = async () => {
setLinkSearchParam({
linkSn: '',
linkNm: '',
startDate: dayjs().subtract(7, "day").format('YYYY-MM-DD'),
endDate: dayjs().format('YYYY-MM-DD'),
});
setLogQueryParam({
isSucc: 'SUCC',
linkNm: '',
linkSn: '',
});
setStartDate(dayjs().subtract(7, "day").format('YYYY-MM-DD'));
setEndDate(dayjs().format('YYYY-MM-DD'));
const data = await LinkApi.getLinkStatsList({
...linkSearchParam,
linkSn: '',
linkNm: '',
startDate: dayjs().subtract(7, "day").format('YYYY-MM-DD'),
endDate: dayjs().format('YYYY-MM-DD'),
});
setLinkStatsList(data);
};
const handleSubmit = async (e?: React.FormEvent<HTMLFormElement>) => {
e?.preventDefault();
if (startDate && endDate) {
if (startDate > endDate) {
alert('종료일이 시작일보다 이전일 수 없습니다.');
return;
} else {
const data = await LinkApi.getLinkStatsList({
...linkSearchParam,
startDate,
endDate
});
setLinkStatsList(data);
setLogQueryParam({
isSucc: 'SUCC',
linkNm: '',
linkSn: '',
});
setLinkSearchParam({
...linkSearchParam,
startDate,
endDate
});
}
} else {
alert('조회 날짜를 선택해주세요.');
return;
}
};
const handleSuccChartClick = async (event: React.MouseEvent<HTMLCanvasElement>) => {
if (!succChartRef.current) return;
const chart = succChartRef.current;
const points = chart.getElementsAtEventForMode(
event.nativeEvent,
"nearest",
{ intersect: true },
false
);
if (points[0] && linkStatsList?.linkSuccStats) {
const index = points[0].index;
setPagination({
...pagination,
pageIndex: 0,
pageSize: 100,
});
setLogQueryParam({
isSucc: 'SUCC',
linkNm: linkStatsList.linkSuccStats[index]?.link_nm,
linkSn: linkStatsList.linkSuccStats[index]?.link_sn,
});
}
};
const handleFailChartClick = async (event: React.MouseEvent<HTMLCanvasElement>) => {
if (!failChartRef.current) return;
const chart = failChartRef.current;
const points = chart.getElementsAtEventForMode(
event.nativeEvent,
"nearest",
{ intersect: true },
false
);
if (points[0] && linkStatsList?.linkFailStats) {
const index = points[0].index;
setPagination({
...pagination,
pageIndex: 0,
pageSize: 100,
});
setLogQueryParam({
isSucc: 'FAIL',
linkNm: linkStatsList.linkFailStats[index]?.link_nm,
linkSn: linkStatsList.linkFailStats[index]?.link_sn,
});
}
};
const handleSubmitToday = async () => {
const data = await LinkApi.getLinkStatsList({
...linkSearchParam,
startDate: dayjs().format('YYYY-MM-DD'),
endDate: dayjs().format('YYYY-MM-DD'),
});
setEndDate(dayjs().format('YYYY-MM-DD'));
setStartDate(dayjs().format('YYYY-MM-DD'));
setLinkStatsList(data);
setLinkSearchParam({
...linkSearchParam,
startDate: dayjs().format('YYYY-MM-DD'),
endDate: dayjs().format('YYYY-MM-DD'),
});
setLogQueryParam({
isSucc: 'SUCC',
linkNm: '',
linkSn: '',
});
};
useEffect(() => {
const fetchData = async () => {
const data = await LinkApi.getLinkStatsList({
...linkSearchParam,
startDate,
endDate
});
setLinkStatsList(data);
};
setLinkSearchParam({
...linkSearchParam,
startDate,
endDate
});
fetchData();
}, []);
return (
<div className="card !mb-0 !min-h-[calc(100vh-16.8rem)]">
<div className="card card-border">
<form className="flex items-center gap-2 p-2" onSubmit={handleSubmit}>
<Label htmlFor="sDateSearchInput" className="mx-2 text-label">
시작일
</Label>
<DatePicker id="sDateSearchInput" mode={"single"} selected={dayjs(startDate).toDate()} onSelect={(e) => setStartDate(dayjs(e).format('YYYY-MM-DD'))} />
<Label htmlFor="eDateSearchInput" className="mx-2 text-label">
종료일
</Label>
<DatePicker id="eDateSearchInput" mode={"single"} className="mr-2" selected={dayjs(endDate).toDate()} onSelect={(e) => setEndDate(dayjs(e).format('YYYY-MM-DD'))} />
<Button color="green" className="h-[3.2rem]" type="button" onClick={handleResetSearchParam}>
초기화
</Button>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button className="h-[3.2rem]" type="button" onClick={handleSubmitToday}>
오늘 날짜로 조회
</Button>
</form>
</div>
<div className="card card-border">
<div className="card !m-[1.2rem] !p-0 flex flex-col gap-y-8">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">조회 일자 : {`${linkSearchParam.startDate} ~ ${linkSearchParam.endDate}`}</h3>
</div>
<div className="h-[32rem] w-full">
<Line ref={succChartRef} data={succData} options={succOptions} onClick={handleSuccChartClick} />
</div>
<div className="h-[32rem]">
<Line ref={failChartRef} data={failData} options={failOptions} onClick={handleFailChartClick} />
</div>
</div>
<div className="card !m-[1.2rem] !p-0">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">
{logQueryParam?.linkNm ? `${logQueryParam.linkNm} 연계 ${logQueryParam.isSucc === 'FAIL' ? '실패' : '성공'} 로그 목록 (${linkSearchParam.startDate} ~ ${linkSearchParam.endDate}) - ${query.data?.totalElements.toLocaleString('ko-KR') ?? 0}건` : '로그 목록'}
</h3>
</div>
<div className="card !m-[1.2rem] h-[40rem] overflow-auto !p-0">
{logQueryParam?.linkNm ?
query.isLoading ?
<div className="[data-theme='black']:bg-transparent rounded-lg p-10 h-full flex justify-center items-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
:
<div>{tableComponent}</div>
:
<div className="flex items-center justify-center h-full text-gray-500 text-3xl">
차트 데이터를 선택해주세요.
</div>
}
</div>
</div>
</div>
</div>
)
};
export { LinkStatsPage };
+1
View File
@@ -0,0 +1 @@
export { LinkTablePage} from "./ui/link-table-page";
@@ -0,0 +1,103 @@
import { createColumnHelper } from "@tanstack/react-table";
import { type LinkTableColumns } from "@/entities/link";
import { Checkbox } from "@common/components";
const columnHelper = createColumnHelper<LinkTableColumns>();
function tableColumns({ selectedColIds, setSelectedColIds }: {
selectedColIds: string[];
setSelectedColIds: React.Dispatch<React.SetStateAction<string[]>>;
}) {
return [
columnHelper.display({
id: "select",
header: ({ table }) => {
const allRows = table.getRowModel().rows;
const allSelected =
allRows.length > 0 &&
allRows.every((row) =>
selectedColIds.includes(row.original.col_sn)
);
return (
<div className="flex justify-center items-center">
<Checkbox
className="bg-white"
checked={
allSelected
? true
: false
}
onCheckedChange={(checked) => {
if (checked) {
setSelectedColIds(allRows.map((row) => row.original.col_sn));
} else {
setSelectedColIds([]);
}
}}
/>
</div>
);
},
cell: ({ row }) => {
const id = row.original.col_sn;
const checked = selectedColIds.includes(id);
return (
<div className="flex justify-center items-center">
<Checkbox
checked={checked}
onClick={(e) => e.stopPropagation()}
onCheckedChange={(checked) => {
setSelectedColIds((prev) =>
checked ? [...prev, id] : prev.filter((x) => x !== id)
);
}}
/>
</div>
);
},
}),
columnHelper.accessor('col_sn', {
id: 'col_sn',
header: '컬럼 일련번호'
}),
columnHelper.accessor('sou_col_nm', {
id: 'sou_col_nm',
header: '원천컬럼명',
cell: (info) => info.getValue() ? info.getValue() : '-',
}),
columnHelper.accessor('sou_col_korn_nm', {
id: 'sou_col_korn_nm',
header: '원천컬럼 한글명',
cell: (info) => info.getValue() ? info.getValue() : '-',
}),
columnHelper.accessor('sou_col_sz', {
id: 'sou_col_sz',
header: '원천컬럼 크기',
cell: (info) => info.getValue() ? info.getValue() : '-',
}),
columnHelper.accessor('trsf_col_nm', {
id: 'trsf_col_nm',
header: '변환컬럼명',
cell: (info) => info.getValue() ? info.getValue() : '-',
}),
columnHelper.accessor('trsf_col_korn_nm', {
id: 'trsf_col_korn_nm',
header: '변환컬럼 한글명',
cell: (info) => info.getValue() ? info.getValue() : '-',
}),
columnHelper.accessor('trsf_col_sz', {
id: 'trsf_col_sz',
header: '변환컬럼 크기',
cell: (info) => info.getValue() ? info.getValue() : '-',
}),
columnHelper.accessor('pk_yn', {
id: 'pk_yn',
header: 'PK 여부',
cell: (info) => info.getValue() === 'Y' ? '예' : '아니오',
}),
]
}
export { tableColumns };
@@ -0,0 +1,503 @@
'use client';
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogClose,
Button,
Input,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
Checkbox
} from "@common/components";
import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form";
import { type LinkTableColData, type LinkTable } from "@/entities/link";
import dayjs from "dayjs";
import type { UseFormReturn } from "react-hook-form";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useState, useEffect } from "react";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
interface LinkTableDialogProps {
dialogOpen: boolean;
onOpenChange: (val: boolean) => void;
form: UseFormReturn<LinkTableColData>;
tableInfo: LinkTable;
insertLinkTableCol: (data: LinkTableColData) => void;
updateLinkTableCol: (data: LinkTableColData) => void;
}
function LinkTableDialog({ dialogOpen, onOpenChange, form, tableInfo, insertLinkTableCol, updateLinkTableCol }: LinkTableDialogProps) {
const isModifyMode = !!form.getValues('col_sn');
const { confirm } = useConfirm();
const [onConfirm, setOnConfirm] = useState<boolean>(false);
const [useColSeq, setUseColSeq] = useState<boolean>(false);
const handleConfirm = async (callback: () => Promise<void>) => {
setOnConfirm(true);
await callback();
setOnConfirm(false);
};
const handleSubmit = async (data: LinkTableColData) => {
const confirmMsg = isModifyMode ? '수정 하시겠습니까?' : '저장 하시겠습니까?';
await handleConfirm(async () => {
if (await confirm(confirmMsg)) {
if (!isModifyMode) {
insertLinkTableCol({
...data,
link_sn: tableInfo.link_sn,
tbl_sn: tableInfo.tbl_sn,
});
} else {
updateLinkTableCol({
...data,
link_sn: tableInfo.link_sn,
tbl_sn: tableInfo.tbl_sn,
});
}
}
});
};
const handleCheckColSeqValue = (check: boolean | string) => {
if (check) {
setUseColSeq(false);
form.setValue('col_seq', null);
} else {
setUseColSeq(true);
}
};
useEffect(() => {
if (!dialogOpen) {
setUseColSeq(false);
form.reset({
tbl_sn: '',
col_sn: '',
sou_col_nm: '',
sou_col_korn_nm: '',
sou_col_data_frm: '',
sou_col_sz: null,
trsf_col_nm: '',
trsf_col_korn_nm: '',
trsf_col_data_frm: '',
trsf_col_sz: null,
col_seq: null,
pk_yn: 'N',
mdfcn_dt: '',
mdfr: '',
});
}
if (isModifyMode && form.getValues('col_seq')) {
setUseColSeq(true);
}
}, [dialogOpen]);
return (
<Dialog
open={dialogOpen}
onOpenChange={(open) => {
onOpenChange(open);
}}
>
<DialogTrigger asChild>
<Button color="green" className="h-[3.2rem]" onClick={() => onOpenChange(true)}>행 추가</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[55rem]">
<DialogHeader>
<DialogTitle>
{tableInfo.trsf_tbl_korn_nm} ({tableInfo.tbl_sn})
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)}>
<div className="card">
<Table variant="secondary">
<colgroup>
<col width="30%" />
<col width="70%" />
</colgroup>
<TableBody>
{isModifyMode &&
<TableRow>
<TableHead>컬럼 일련번호</TableHead>
<TableCell>
<FormField
control={form.control}
name="col_sn"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
disabled
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
}
<TableRow>
<TableHead>* 원천컬럼명</TableHead>
<TableCell>
<FormField
control={form.control}
name="sou_col_nm"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
autoComplete="off"
/>
</FormControl>
{form.formState.errors.sou_col_nm && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.sou_col_nm.message}
</p>
)}
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>* 원천컬럼 한글명</TableHead>
<TableCell>
<FormField
control={form.control}
name="sou_col_korn_nm"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
autoComplete="off"
/>
</FormControl>
{form.formState.errors.sou_col_korn_nm && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.sou_col_korn_nm.message}
</p>
)}
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>* 원천컬럼 데이터 형식</TableHead>
<TableCell>
<FormField
control={form.control}
name="sou_col_data_frm"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
autoComplete="off"
/>
</FormControl>
{form.formState.errors.sou_col_data_frm && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.sou_col_data_frm.message}
</p>
)}
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>원천컬럼 크기</TableHead>
<TableCell>
<FormField
control={form.control}
name="sou_col_sz"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
autoComplete="off"
type="number"
min={0}
max={99999999999999}
onChange={(e) => {
const onlyDigits = e.target.value.replace(/\D/g, "").slice(0, 14);
field.onChange(onlyDigits); // react-hook-form에 반영
}}
/>
</FormControl>
<p className="text-gray-500 text-base pt-2">
※ 14자리까지 입력 가능합니다.
</p>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>PK 여부</TableHead>
<TableCell>
<FormField
control={form.control}
name="pk_yn"
render={({ field }) => (
<FormItem>
<FormControl>
<Select value={field.value ?? "N"} onValueChange={field.onChange}>
<SelectTrigger id="liResultCode" className="w-[10rem] mr-4">
<SelectValue placeholder="아니오"></SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="Y">예</SelectItem>
<SelectItem value="N">아니오</SelectItem>
</SelectContent>
</Select>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>* 변환컬럼명</TableHead>
<TableCell>
<FormField
control={form.control}
name="trsf_col_nm"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
autoComplete="off"
/>
</FormControl>
{form.formState.errors.trsf_col_nm && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.trsf_col_nm.message}
</p>
)}
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>* 변환컬럼 한글명</TableHead>
<TableCell>
<FormField
control={form.control}
name="trsf_col_korn_nm"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
autoComplete="off"
/>
</FormControl>
{form.formState.errors.trsf_col_korn_nm && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.trsf_col_korn_nm.message}
</p>
)}
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>* 변환컬럼 데이터 형식</TableHead>
<TableCell>
<FormField
control={form.control}
name="trsf_col_data_frm"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
autoComplete="off"
/>
</FormControl>
{form.formState.errors.trsf_col_data_frm && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.trsf_col_data_frm.message}
</p>
)}
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>변환컬럼 크기</TableHead>
<TableCell>
<FormField
control={form.control}
name="trsf_col_sz"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
autoComplete="off"
type="number"
min={0}
max={99999999999999}
onChange={(e) => {
const onlyDigits = e.target.value.replace(/\D/g, "").slice(0, 14);
field.onChange(onlyDigits); // react-hook-form에 반영
}}
/>
</FormControl>
<p className="text-gray-500 text-base pt-2">
※ 14자리까지 입력 가능합니다.
</p>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>컬럼 매칭 순서</TableHead>
<TableCell>
<FormField
control={form.control}
name="col_seq"
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-x-2 mb-4">
<Checkbox id="useColSeq" className="!border-gray-600" checked={!useColSeq} onCheckedChange={(check) => handleCheckColSeqValue(check)} />
<label htmlFor="useColSeq" className="text-gray-600">사용안함</label>
</div>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
autoComplete="off"
type="number"
min={0}
disabled={!useColSeq}
max={999}
onChange={(e) => {
const onlyDigits = e.target.value.replace(/\D/g, "").slice(0, 3);
field.onChange(onlyDigits); // react-hook-form에 반영
}}
/>
</FormControl>
<p className="text-gray-500 text-base pt-2">
※ 3자리까지 입력 가능합니다.
</p>
</FormItem>
)}
/>
</TableCell>
</TableRow>
{isModifyMode &&
<>
<TableRow>
<TableHead>수정자</TableHead>
<TableCell>
<FormField
control={form.control}
name="mdfr"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
disabled
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>수정일시</TableHead>
<TableCell>
<FormField
control={form.control}
name="mdfcn_dt"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ? dayjs(field.value).format('YYYY-MM-DD') : ""}
className="!text-[1.3rem]"
disabled
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
</>
}
</TableBody>
</Table>
</div>
<DialogFooter>
<Button type="submit">저장</Button>
<DialogClose asChild>
<Button color="white">취소</Button>
</DialogClose>
</DialogFooter>
</form>
</Form>
<ConfirmDialog isOpen={onConfirm} />
</DialogContent>
</Dialog>
)
}
export { LinkTableDialog };
@@ -0,0 +1,710 @@
'use client';
import { useState, useMemo } from "react";
import {
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 { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { columns } from "../../linkParams/model/table-columns";
import { tableColumns } from "../model/table-columns";
import { LinkTableDialog } from "./link-table-dialog";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import { AUTH_USER_ID } from "@/shared/config";
import { getCookie } from "@common/components";
import { useAlertStore } from "@/shared/lib/use-alert-store";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form";
import { useDataTable } from "@/shared/lib";
function LinkTablePage() {
const queryClient = useQueryClient();
const userUniqId = getCookie(AUTH_USER_ID) ?? "";
const [linkSn, setLinkSn] = useState<string>('');
const [linkNm, setLinkNm] = useState<string>('');
const [linkSearchParam, setLinkSearchParam] = useState<LinkSearchParam>({
linkSn: '',
linkNm: ''
});
const [selectedTableInfo, setSelectedTableInfo] = useState<LinkTable | null>(null);
const [tableColList, setTableColList] = useState<LinkTableColumns[]>([]);
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const [selectedColIds, setSelectedColIds] = useState<string[]>([]);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 100,
});
const { setMessage: alert } = useAlertStore((state) => state);
const { confirm } = useConfirm();
const [onConfirm, setOnConfirm] = useState<boolean>(false);
const handleConfirm = async (callback: () => Promise<void>) => {
setOnConfirm(true);
await callback();
setOnConfirm(false);
};
const query = useQuery({
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/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: 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: 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<LinkTableColData>({
resolver: zodResolver(LinkTableColumnsSchema),
defaultValues: {
pk_yn: "N"
}
});
const tableForm = useForm<LinkTableFormData>({
resolver: zodResolver(LinkTableSchema),
defaultValues: {},
});
const handleResetSearchParam = async () => {
setSelectedTableInfo(null);
setTableColList([]);
setLinkSn('');
setLinkNm('');
setLinkSearchParam({
...linkSearchParam,
linkSn: '',
linkNm: '',
});
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setSelectedTableInfo(null);
setTableColList([]);
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 });
if (colInfo) {
let newData = { ...colInfo };
if (colInfo.mdfr) {
const userName = await fetchUserInfo();
newData = {
...colInfo,
mdfr: 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: 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: 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 (
<div className="card card-border !mt-6">
<div className="card card-border">
<form className="flex items-center gap-2 p-2" onSubmit={handleSubmit}>
<Label htmlFor="liSnSearchInput" className="mr-2 text-label">
일련번호
</Label>
<Input
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-4"
id="liSnSearchInput"
value={linkSn}
onChange={(e) => setLinkSn(e.target.value)}
autoComplete="off"
/>
<Label htmlFor="liNmSearchInput" className="mr-2 text-label">
연계 명
</Label>
<Input
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-2"
id="liNmSearchInput"
value={linkNm}
onChange={(e) => setLinkNm(e.target.value)}
autoComplete="off"
/>
<Button color="green" className="h-[3.2rem]" type="button" onClick={handleResetSearchParam}>
초기화
</Button>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
</form>
</div>
<div className="card !mb-0 flex h-[calc(100vh-28rem)] gap-5 !p-0">
<div className="card card-border w-1/2 !mx-0 flex flex-col">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">연계 목록</h3>
</div>
<div className="card !m-[1.2rem] overflow-auto !p-0 h-full">
{!query.isLoading ?
<div>{tableComponent}</div>
:
<div className="[data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center h-full">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
}
</div>
</div>
<div className="card card-border w-1/2 !mx-0 flex flex-col">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">연계 테이블 정보</h3>
</div>
<div className="card !m-[1.2rem] !p-0 h-full overflow-auto flex flex-col">
<Form {...tableForm}>
<form onSubmit={tableForm.handleSubmit(handleTableSubmit)}>
{selectedTableInfo &&
<div className="flex justify-end mb-2">
<Button className="h-[3.2rem]" type="submit" color={selectedTableInfo.tbl_sn ? "green" : "primary"}>
{selectedTableInfo.tbl_sn ? "수정" : "저장"}
</Button>
</div>
}
<Table variant="secondary" className="flex-none">
<colgroup>
<col width="20%" />
<col width="80%" />
</colgroup>
<TableBody>
<TableRow>
<TableHead>연계 일련번호</TableHead>
<TableCell className="!p-2">
{selectedTableInfo ?
<FormField
control={tableForm.control}
name="link_sn"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem] w-[400px]"
disabled
/>
</FormControl>
</FormItem>
)}
/>
:
<span className="px-2 py-[1.2rem]">-</span>
}
</TableCell>
</TableRow>
<TableRow>
<TableHead>연계 명</TableHead>
<TableCell className="!p-2">
{selectedTableInfo ?
<FormField
control={tableForm.control}
name="link_nm"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem] w-[400px]"
disabled
/>
</FormControl>
</FormItem>
)}
/>
:
<span className="px-2 py-[1.2rem]">-</span>
}
</TableCell>
</TableRow>
<TableRow>
<TableHead>원천 테이블 명</TableHead>
<TableCell className="!p-2">
{selectedTableInfo ?
<FormField
control={tableForm.control}
name="sou_tbl_nm"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem] w-[400px]"
placeholder={!field.value ? "-" : ""}
autoComplete="off"
/>
</FormControl>
{tableForm.formState.errors.sou_tbl_nm && (
<p className="text-red-500 text-base pt-2">
{tableForm.formState.errors.sou_tbl_nm.message}
</p>
)}
</FormItem>
)}
/>
:
<span className="px-2 py-[1.2rem]">-</span>
}
</TableCell>
</TableRow>
<TableRow>
<TableHead>원천 테이블 한글명</TableHead>
<TableCell className="!p-2">
{selectedTableInfo ?
<FormField
control={tableForm.control}
name="sou_tbl_korn_nm"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem] w-[400px]"
placeholder={!field.value ? "-" : ""}
autoComplete="off"
/>
</FormControl>
{tableForm.formState.errors.sou_tbl_korn_nm && (
<p className="text-red-500 text-base pt-2">
{tableForm.formState.errors.sou_tbl_korn_nm.message}
</p>
)}
</FormItem>
)}
/>
:
<span className="px-2 py-[1.2rem]">-</span>
}
</TableCell>
</TableRow>
<TableRow>
<TableHead>원천 테이블 설명</TableHead>
<TableCell className="!p-2">
{selectedTableInfo ?
<FormField
control={tableForm.control}
name="sou_tbl_expln"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem] w-[400px]"
placeholder={!field.value ? "-" : ""}
autoComplete="off"
/>
</FormControl>
{tableForm.formState.errors.sou_tbl_expln && (
<p className="text-red-500 text-base pt-2">
{tableForm.formState.errors.sou_tbl_expln.message}
</p>
)}
</FormItem>
)}
/>
:
<span className="px-2 py-[1.2rem]">-</span>
}
</TableCell>
</TableRow>
<TableRow>
<TableHead>* 변환 테이블 명</TableHead>
<TableCell className="!p-2">
{selectedTableInfo ?
<FormField
control={tableForm.control}
name="trsf_tbl_nm"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem] w-[400px]"
placeholder={!field.value ? "-" : ""}
autoComplete="off"
/>
</FormControl>
{tableForm.formState.errors.trsf_tbl_nm && (
<p className="text-red-500 text-base pt-2">
{tableForm.formState.errors.trsf_tbl_nm.message}
</p>
)}
</FormItem>
)}
/>
:
<span className="px-2 py-[1.2rem]">-</span>
}
</TableCell>
</TableRow>
<TableRow>
<TableHead>* 변환 테이블 한글명</TableHead>
<TableCell className="!p-2">
{selectedTableInfo ?
<FormField
control={tableForm.control}
name="trsf_tbl_korn_nm"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem] w-[400px]"
placeholder={!field.value ? "-" : ""}
autoComplete="off"
/>
</FormControl>
{tableForm.formState.errors.trsf_tbl_korn_nm && (
<p className="text-red-500 text-base pt-2">
{tableForm.formState.errors.trsf_tbl_korn_nm.message}
</p>
)}
</FormItem>
)}
/>
:
<span className="px-2 py-[1.2rem]">-</span>
}
</TableCell>
</TableRow>
<TableRow>
<TableHead>변환 테이블 설명</TableHead>
<TableCell className="!p-2">
{selectedTableInfo ?
<FormField
control={tableForm.control}
name="trsf_tbl_expln"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem] w-[400px]"
placeholder={!field.value ? "-" : ""}
autoComplete="off"
/>
</FormControl>
{tableForm.formState.errors.trsf_tbl_expln && (
<p className="text-red-500 text-base pt-2">
{tableForm.formState.errors.trsf_tbl_expln.message}
</p>
)}
</FormItem>
)}
/>
:
<span className="px-2 py-[1.2rem]">-</span>
}
</TableCell>
</TableRow>
</TableBody>
</Table>
</form>
</Form>
{selectedTableInfo ?
<>
<div className="mb-2 mt-14 flex justify-end items-center flex-none">
{tableColList.length > 0 &&
<Button color="red" className="h-[3.2rem] mr-2" type="button" onClick={handleDeleteParam}>
행 삭제
</Button>
}
{selectedTableInfo.tbl_sn &&
<LinkTableDialog dialogOpen={dialogOpen} onOpenChange={setDialogOpen} form={form} tableInfo={selectedTableInfo} insertLinkTableCol={insertLinkTableCol} updateLinkTableCol={updateLinkTableCol} />
}
</div>
{selectedTableInfo.tbl_sn ?
(tableColList.length > 0 ?
<div className="flex-1 overflow-auto min-h-[300px]">
<DataTable table={colTable} onRowClick={handleColumnRowClick} />
</div>
:
<div className="flex items-center justify-center h-20 text-gray-500 text-3xl">
등록된 테이블 컬럼이 없습니다.
</div>)
:
<div className="flex items-center justify-center h-20 text-gray-500 text-3xl">
테이블 정보를 등록해주세요.
</div>
}
</>
:
<div className="flex items-center justify-center text-gray-500 text-3xl mt-14 flex-1">
연계 데이터를 선택해주세요.
</div>
}
</div>
</div>
</div>
<ConfirmDialog isOpen={onConfirm} />
</div >
)
};
export { LinkTablePage };
+33 -27
View File
@@ -6,7 +6,6 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import {
Button,
DateRangePicker,
Input,
Label,
Select,
@@ -16,14 +15,14 @@ import {
SelectValue,
LoadingWithImage,
} from "@common/components";
import { LoginlogApi } from "@/entities/loginlog";
import { type LoginlogParams } from "@/entities/loginlog/model/loginlog-interface";
import { columns } from "@/pages/loginlog/model/table-columns";
import { LoginlogApi, type LoginlogParams } from "@/entities/loginlog";
import { useDataTable } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
import { columns } from "@/pages/loginlog/model/table-columns";
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
type DateRange = {
from: Date | undefined;
from?: Date | undefined;
to?: Date | undefined;
};
@@ -33,13 +32,12 @@ function LoginlogPage() {
to: dayjs().toDate(),
};
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
const [isManualLoading, setIsManualLoading] = useState(false);
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [presetDateRange, setPresetDateRange] = useState("");
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
const [userId, setUserId] = useState("");
const [errOcrnYn, setErrOcrnYn] = useState("N");
const [searchParams, setSearchParams] = useState<LoginlogParams>({
@@ -49,6 +47,7 @@ function LoginlogPage() {
searchDateTo: dayjs().format("YYYY-MM-DD"),
sort: "lgnDt,desc",
});
const [isManualLoading, setIsManualLoading] = useState(false);
const query = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.LOGINLOG, searchParams, pagination],
@@ -89,8 +88,12 @@ function LoginlogPage() {
...searchParams,
userId,
errOcrnYn,
searchDateFrom: dayjs(dateRange.from).format("YYYY-MM-DD"),
searchDateTo: dayjs(dateRange.to).format("YYYY-MM-DD"),
searchDateFrom: dateRange.from
? dayjs(dateRange.from).format("YYYY-MM-DD")
: "",
searchDateTo: dateRange.to
? dayjs(dateRange.to).format("YYYY-MM-DD")
: "",
});
query.refetch().finally(() => {
setIsManualLoading(false);
@@ -134,31 +137,34 @@ function LoginlogPage() {
default:
presetDateRange = undefined;
}
setPresetDateRange(value);
setDateRange(presetDateRange || defaultDateRange);
};
const handleDateRangeChange = (dateRange: DateRange) => {
setDateRange(dateRange);
setPresetDateRange("");
};
return (
<div className="flex-1">
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2">조회기간</Label>
<DateRangePicker
value={dateRange}
onValueChange={(value) => setDateRange(value || defaultDateRange)}
>
<div className="card card-border flex items-center gap-2">
<Select onValueChange={handlePresetDateRange}>
<SelectTrigger>
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1W">일주일</SelectItem>
<SelectItem value="1M">1개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem>
</SelectContent>
</Select>
</div>
</DateRangePicker>
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1W">일주일</SelectItem>
<SelectItem value="1M">1개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem>
</SelectContent>
</Select>
<InputDateRangePicker
dateRange={dateRange}
setDateRange={handleDateRangeChange}
/>
<Label className="ml-4 mr-2 text-label">사용자ID</Label>
<Input
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
+1 -1
View File
@@ -3,7 +3,7 @@ import type {
TreeUserOrganization,
TreeUser,
} from "@/entities/user-organization";
import type { TreeMenu, TreeNode } from "../model/tree-menu-interface";
import type { TreeMenu, TreeNode } from "@/entities/tree-menu";
export const generateMenuTreeData = (
data: TreeMenu[],
+29 -16
View File
@@ -1,5 +1,6 @@
import type { ComponentProps } from "react";
import { Button, Input } from "@common/components";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
type EditableNodeProps = {
depth?: number;
@@ -20,22 +21,34 @@ function EditableNode({
>
<Input {...props} className="h-[2.5rem] !text-[1.3rem] text-foreground" />
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="size-8 !bg-transparent"
onClick={onSave}
>
<i className="diveicon di-check text-2xl" />
</Button>
<Button
variant="ghost"
size="icon"
className="size-8 !bg-transparent"
onClick={onCancel}
>
<i className="diveicon di-x" />
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-8 !bg-transparent"
onClick={onSave}
aria-label="저장"
>
<i className="diveicon di-check text-2xl" />
</Button>
</TooltipTrigger>
<TooltipContent>저장</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-8 !bg-transparent"
onClick={onCancel}
aria-label="취소"
>
<i className="diveicon di-x" />
</Button>
</TooltipTrigger>
<TooltipContent>취소</TooltipContent>
</Tooltip>
</div>
</div>
);
+1 -2
View File
@@ -2,10 +2,9 @@ import { useEffect, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import type { NodeModel, TreeMethods } from "@minoru/react-dnd-treeview";
import { Button, cn } from "@common/components";
import { TreeMenuApi, type TreeMenu } from "@/entities/tree-menu";
import { useMutationWrapper } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
import { TreeMenuApi } from "../api/tree-menu-service";
import type { TreeMenu } from "../model/tree-menu-interface";
import { EditableNode } from "./editable-node";
type MenuNodeProps = {
+2 -2
View File
@@ -21,11 +21,10 @@ import {
type MenuFormData,
} from "@/entities/menu";
import { SiteApi } from "@/entities/site";
import { TreeMenuApi, type TreeMenu } from "@/entities/tree-menu";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import { generateMenuTreeData } from "../lib/utils";
import { TreeMenuApi } from "../api/tree-menu-service";
import type { TreeMenu } from "../model/tree-menu-interface";
import { MenuTree } from "./menu-tree";
import { MenuForm } from "./menu-form";
@@ -111,6 +110,7 @@ function MenuPage() {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
});
form.reset();
},
});
+5 -2
View File
@@ -12,8 +12,11 @@ import { Button } from "@common/components";
import { ENDPOINT } from "@/shared/config";
import { useMutationWrapper } from "@/shared/lib";
import type { MenuFormData } from "@/entities/menu";
import { TreeMenuApi } from "../api/tree-menu-service";
import type { TreeMenu, TreeNode } from "../model/tree-menu-interface";
import {
TreeMenuApi,
type TreeMenu,
type TreeNode,
} from "@/entities/tree-menu";
import { handleDropNode, handleSelectNode } from "../lib/utils";
import { MenuNode } from "./menu-node";
import { EditableNode } from "./editable-node";
@@ -72,7 +72,7 @@ function AddUserDialog({ onAddUsers, ...props }: AddUserDialogProps) {
</DialogTrigger>
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>콘텐츠 조회</DialogTitle>
<DialogTitle>사용자 조회</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4">
<div className="card card-border !m-0 flex w-full items-center gap-2">
@@ -242,6 +242,16 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) {
</div>
<div className="card max-h-[calc(100vh-58rem)] overflow-auto !p-0">
<Table>
<colgroup>
<col width="5%" />
<col width="5%" />
<col width="auto" />
<col width="10%" />
<col width="10%" />
<col width="10%" />
<col width="15%" />
<col width="15%" />
</colgroup>
<TableHeader className="sticky top-0">
<TableRow>
<TableHead>
@@ -300,6 +310,9 @@ const UserCells = memo(
jobPositionCodeData,
jobGradeCodeData,
}: UserCellsProps) => {
const [jbpsSeCd, setJbpsSeCd] = useState(user.jbpsSeCd);
const [jbgdSeCd, setJbgdSeCd] = useState(user.jbgdSeCd);
const updateMode = (index: number) => {
const mode = form.watch(`userOrganization.${index}.mode`);
if (mode === "R") {
@@ -322,17 +335,33 @@ const UserCells = memo(
<CodeSelect
codeData={jobPositionCodeData}
defaultValue={user.jbpsSeCd}
onValueChange={updateFormValue(index, "jbpsSeCd")}
onValueChange={(value) => {
setJbpsSeCd(value);
updateFormValue(index, "jbpsSeCd")(value);
}}
{...form.register(`userOrganization.${index}.jbpsSeCd`)}
/>
{!jbpsSeCd && (
<p className="pt-1 text-base text-destructive">
직위를 선택해주세요.
</p>
)}
</TableCell>
<TableCell align="center">
<CodeSelect
codeData={jobGradeCodeData}
defaultValue={user.jbgdSeCd}
onValueChange={updateFormValue(index, "jbgdSeCd")}
onValueChange={(value) => {
setJbgdSeCd(value);
updateFormValue(index, "jbgdSeCd")(value);
}}
{...form.register(`userOrganization.${index}.jbgdSeCd`)}
/>
{!jbgdSeCd && (
<p className="pt-1 text-base text-destructive">
직급을 선택해주세요.
</p>
)}
</TableCell>
<TableCell align="center">
<Input
@@ -33,9 +33,14 @@ export const columns: ColumnDef<Post>[] = [
accessorKey: "pstgPrdSeNm",
header: "게시기간",
cell: ({ row }) => {
const pstgPrdSeCd = row.original.pstgPrdSeCd;
const pstgPrdSeNm = row.original.pstgPrdSeNm;
const pstgBgngDt = row.original.pstgBgngDt;
const pstgEndDt = row.original.pstgEndDt;
if (pstgPrdSeCd === "99") {
return pstgPrdSeNm;
}
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
},
},
@@ -6,7 +6,6 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import {
Button,
DateRangePicker,
Input,
Label,
Select,
@@ -15,14 +14,17 @@ import {
SelectTrigger,
SelectValue,
} from "@common/components";
import { PrivacyLogPrivacy } from "@/entities/privacylog";
import type { PrivacyLogParams } from "@/entities/privacylog/model/privacylog-interface";
import { columns } from "@/pages/privacylog/model/table-columns";
import {
PrivacyLogPrivacy,
type PrivacyLogParams,
} from "@/entities/privacylog";
import { useDataTable } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
import { columns } from "@/pages/privacylog/model/table-columns";
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
type DateRange = {
from: Date | undefined;
from?: Date | undefined;
to?: Date | undefined;
};
@@ -36,6 +38,7 @@ function PrivacyLogPage() {
pageIndex: 0,
pageSize: 10,
});
const [presetDateRange, setPresetDateRange] = useState("");
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
const [inqInfoKeyword, setInqInfoKeyword] = useState("");
const [searchParams, setSearchParams] = useState<PrivacyLogParams>({
@@ -83,8 +86,10 @@ function PrivacyLogPage() {
setSearchParams({
...searchParams,
inqInfoKeyword,
inqDtStart: dayjs(dateRange.from).format("YYYY-MM-DD"),
inqDtEnd: dayjs(dateRange.to).format("YYYY-MM-DD"),
inqDtStart: dateRange.from
? dayjs(dateRange.from).format("YYYY-MM-DD")
: "",
inqDtEnd: dateRange.to ? dayjs(dateRange.to).format("YYYY-MM-DD") : "",
});
};
@@ -123,38 +128,40 @@ function PrivacyLogPage() {
default:
presetDateRange = undefined;
}
setPresetDateRange(value);
setDateRange(presetDateRange || defaultDateRange);
};
const handleDateRangeChange = (dateRange: DateRange) => {
setDateRange(dateRange);
setPresetDateRange("");
};
return (
<div className="flex-1">
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2">조회기간</Label>
<DateRangePicker
value={dateRange}
onValueChange={(value) => setDateRange(value || defaultDateRange)}
>
<div className="card card-border flex items-center gap-2">
<Select onValueChange={handlePresetDateRange}>
<SelectTrigger>
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1W">일주일</SelectItem>
<SelectItem value="1M">1개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem>
</SelectContent>
</Select>
</div>
</DateRangePicker>
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1W">일주일</SelectItem>
<SelectItem value="1M">1개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem>
</SelectContent>
</Select>
<InputDateRangePicker
dateRange={dateRange}
setDateRange={handleDateRangeChange}
/>
<Label className="ml-4 mr-2 text-label">조회정보</Label>
<Input
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
value={inqInfoKeyword}
onChange={(e) => setInqInfoKeyword(e.target.value)}
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
+33 -28
View File
@@ -12,7 +12,6 @@ import {
} from "@tanstack/react-table";
import {
Button,
DateRangePicker,
Input,
Label,
Select,
@@ -31,13 +30,14 @@ import {
} from "@/entities/post";
import { BoardApi } from "@/entities/board";
import { AuthorityButton } from "@/entities/menu-authority";
import { useAlertStore, useSelectPrompt, useDataTable } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
import { useAlertStore, useSelectPrompt, useDataTable } from "@/shared/lib";
import { columns } from "../model/table-columns";
import { MovePostDialog } from "./move-post-dialog";
type DateRange = {
from: Date | undefined;
from?: Date | undefined;
to?: Date | undefined;
};
@@ -51,22 +51,21 @@ function RecentPage() {
const { confirm } = useConfirm();
const alert = useAlertStore((state) => state.setMessage);
const [dateRange, setDateRange] = useState<DateRange | undefined>({
from: undefined,
to: undefined,
});
const [tableSort, setTableSort] = useState("regDt,desc");
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [presetDateRange, setPresetDateRange] = useState("");
const [dateRange, setDateRange] = useState<DateRange>({
from: undefined,
to: undefined,
});
const [keywordType, setKeywordType] = useState("");
const [keyword, setKeyword] = useState("");
const [bbsId, setBbsId] = useState<string | null>(null);
const [delYn, setDelYn] = useState("N");
const [searchParams, setSearchParams] = useState<RecentPostParams>({
keywordType: "",
keyword: "",
@@ -170,7 +169,7 @@ function RecentPage() {
};
const handlePresetDateRange = (value: string) => {
let presetDateRange: DateRange | undefined;
let presetDateRange: DateRange;
switch (value) {
case "1W":
presetDateRange = {
@@ -191,11 +190,20 @@ function RecentPage() {
};
break;
default:
presetDateRange = undefined;
presetDateRange = {
from: undefined,
to: undefined,
};
}
setPresetDateRange(value);
setDateRange(presetDateRange);
};
const handleDateRangeChange = (dateRange: DateRange) => {
setDateRange(dateRange);
setPresetDateRange("");
};
const onSoftDeletePost = async () => {
const { resolved, value: delRsn } =
await prompt("삭제 사유를 입력해주세요.");
@@ -250,21 +258,20 @@ function RecentPage() {
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2">날짜</Label>
<DateRangePicker value={dateRange} onValueChange={setDateRange}>
<div className="card card-border flex items-center gap-2">
<Select onValueChange={handlePresetDateRange}>
<SelectTrigger>
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1W">일주일</SelectItem>
<SelectItem value="1M">1개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem>
</SelectContent>
</Select>
</div>
</DateRangePicker>
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1W">일주일</SelectItem>
<SelectItem value="1M">1개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem>
</SelectContent>
</Select>
<InputDateRangePicker
dateRange={dateRange}
setDateRange={handleDateRangeChange}
/>
<Label className="ml-4 mr-2 text-label">게시판</Label>
<Select
value={bbsId?.toString() || ""}
@@ -281,7 +288,6 @@ function RecentPage() {
))}
</SelectContent>
</Select>
<Label className="ml-4 mr-2 text-label">상태</Label>
<Select value={delYn} onValueChange={(value) => setDelYn(value)}>
<SelectTrigger className="w-[10rem]">
@@ -292,7 +298,6 @@ function RecentPage() {
<SelectItem value="Y">삭제</SelectItem>
</SelectContent>
</Select>
<Label className="ml-4 mr-2 text-label">검색</Label>
<Select
value={keywordType}
@@ -0,0 +1,28 @@
import { client } from "@/shared/api";
import type {
SiteAuthorityCreatePayload,
SiteAuthorityList,
SiteAuthoritySearchParams,
SiteAuthorityService,
} from "../model/site-authority-interface";
import { ENDPOINT } from "@/shared/config";
export class SiteAuthorityApiService implements SiteAuthorityService {
public async getSiteAuthorityList(
params: SiteAuthoritySearchParams,
): Promise<SiteAuthorityList> {
const response = await client.get<SiteAuthorityList>(
ENDPOINT.CMS_SERVICE.SITE_AUTHORITY,
{ params },
);
return response.data;
}
public async createSiteAuthority(
data: SiteAuthorityCreatePayload[],
): Promise<void> {
await client.post(ENDPOINT.CMS_SERVICE.SITE_AUTHORITY, data);
}
}
export const SiteAuthorityApi = new SiteAuthorityApiService();
@@ -0,0 +1,24 @@
import type { PageableList, PageableParams } from "@/shared/model";
export interface SiteAuthoritySearchParams extends PageableParams {
siteId?: string;
authrtId?: string;
}
export interface SiteAuthority {
siteId: string;
authrtId: string;
siteNm: string;
mode: string;
}
export type SiteAuthorityCreatePayload = Omit<SiteAuthority, "siteNm">;
export type SiteAuthorityList = PageableList<SiteAuthority>;
export interface SiteAuthorityService {
getSiteAuthorityList: (
params: SiteAuthoritySearchParams,
) => Promise<SiteAuthorityList>;
createSiteAuthority: (data: SiteAuthorityCreatePayload[]) => Promise<void>;
}
+18 -2
View File
@@ -3,8 +3,13 @@ import { useState } from "react";
import type { Role } from "@/entities/role";
import type { ColumnDef } from "@tanstack/react-table";
import { MappingRoleDialog } from "../ui/mapping-role-dialog";
import { Button } from "@common/components";
export const columns: ColumnDef<Role>[] = [
export const createColumns = ({
deleteRole,
}: {
deleteRole: (roleId: string) => void;
}): ColumnDef<Role>[] => [
{
id: "no",
header: "NO",
@@ -39,7 +44,18 @@ export const columns: ColumnDef<Role>[] = [
{
id: "action",
header: "관리",
cell: ({ row }) => <MappingRoleButton roleId={row.original.roleId} />,
cell: ({ row }) => (
<div className="flex items-center justify-center gap-2">
<MappingRoleButton roleId={row.original.roleId} />
<Button
size="sm"
color="red"
onClick={() => deleteRole(row.original.roleId)}
>
삭제
</Button>
</div>
),
},
];
@@ -1,5 +1,6 @@
import { getCoreRowModel, type ColumnDef } from "@tanstack/react-table";
import { useState, useRef } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { getCoreRowModel, type ColumnDef } from "@tanstack/react-table";
import {
Dialog,
DialogTrigger,
@@ -8,6 +9,7 @@ import {
DialogTitle,
Button,
Switch,
Input,
} from "@common/components";
import {
RoleAuthorizationsApi,
@@ -31,12 +33,18 @@ function MappingRoleDialog({
}: MappingRoleDialogProps) {
const queryClient = useQueryClient();
const alert = useAlertStore((state) => state.setMessage);
const inputRef = useRef<HTMLInputElement>(null);
const [searchParams, setSearchParams] = useState({
keyword: "",
keywordType: "authorizationName",
});
const query = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS],
queryKey: [ENDPOINT.USER_SERVICE.ROLE_AUTHORIZATIONS, searchParams],
queryFn: () =>
RoleAuthorizationsApi.getRoleAuthorizationsListByRoleId({
roleId,
...searchParams,
}),
enabled: open,
});
@@ -46,7 +54,7 @@ function MappingRoleDialog({
RoleAuthorizationsApi.createRoleAuthorizations(data),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS],
queryKey: [ENDPOINT.USER_SERVICE.ROLE_AUTHORIZATIONS],
});
alert("권한 맵핑이 완료되었습니다.");
},
@@ -66,6 +74,26 @@ function MappingRoleDialog({
);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const keyword = formData.get("keyword") as string;
setSearchParams({
...searchParams,
keyword,
});
};
const handleReset = () => {
if (inputRef.current) {
inputRef.current.value = "";
}
setSearchParams({
...searchParams,
keyword: "",
});
};
const { tableComponent } = useDataTable({
query,
tableOptions: {
@@ -79,21 +107,31 @@ function MappingRoleDialog({
<DialogTrigger asChild>
<Button size="sm">권한맵핑</Button>
</DialogTrigger>
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
<DialogContent className="max-w-[80rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>모듈 목록</DialogTitle>
</DialogHeader>
<div className="max-h-[40rem] overflow-y-auto">
<div className="mb-4 flex justify-end gap-2">
<Button size="sm" onClick={() => handleApplyAll("Y")}>
일괄적용
<div className="flex items-center justify-between">
<form onSubmit={handleSubmit} className="flex items-center gap-2">
<Input
ref={inputRef}
type="text"
name="keyword"
className="w-[32rem]"
/>
<Button type="submit">검색</Button>
<Button type="button" color="white" onClick={handleReset}>
초기화
</Button>
<Button size="sm" color="gray" onClick={() => handleApplyAll("N")}>
</form>
<div className="flex items-center justify-end gap-2">
<Button onClick={() => handleApplyAll("Y")}>일괄적용</Button>
<Button color="gray" onClick={() => handleApplyAll("N")}>
일괄해제
</Button>
</div>
{tableComponent}
</div>
<div className="max-h-[60rem] overflow-y-auto">{tableComponent}</div>
</DialogContent>
</Dialog>
);
+86 -5
View File
@@ -1,7 +1,9 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import type { UseFormReturn } from "react-hook-form";
import {
Button,
Checkbox,
Input,
Label,
RadioGroup,
@@ -16,6 +18,9 @@ import { FormWrapper } from "@/widgets/form-wrapper";
import { RoleApi, type RoleFormData } from "@/entities/role";
import { FormField, FormItem, FormControl } from "@/shared/ui/form";
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
import { SiteAuthorityApi } from "../api/site-authority-service";
import type { SiteAuthorityCreatePayload } from "../model/site-authority-interface";
type RoleFormProps = {
roleId?: string;
@@ -24,12 +29,24 @@ type RoleFormProps = {
handleDelete?: () => Promise<void>;
};
function RoleForm({ roleId, form, ...props }: RoleFormProps) {
function RoleForm({ roleId, form, handleSave, ...props }: RoleFormProps) {
const isModifyMode = !!roleId;
const queryClient = useQueryClient();
const alert = useAlertStore((state) => state.setMessage);
const [isValidRoleId, setIsValidRoleId] = useState(false);
const [checkedSiteAuthority, setCheckedSiteAuthority] = useState<string[]>(
[],
);
const { data: siteAuthorityList } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.SITE_AUTHORITY, roleId],
queryFn: () =>
SiteAuthorityApi.getSiteAuthorityList({
authrtId: roleId,
page: 0,
size: 100,
}),
});
const { mutateAsync: checkDuplicateRoleId } = useMutationWrapper({
mutationFn: (roleId: string) => RoleApi.checkDuplicateRoleId(roleId),
@@ -43,8 +60,49 @@ function RoleForm({ roleId, form, ...props }: RoleFormProps) {
},
});
const handleSiteAuthorityChecked = (checked: boolean, siteId: string) => {
if (checked) {
setCheckedSiteAuthority((prev) => [...prev, siteId]);
} else {
setCheckedSiteAuthority((prev) => prev.filter((id) => id !== siteId));
}
};
const saveInterceptor = async (data: RoleFormData) => {
const siteAuthorityPayload: SiteAuthorityCreatePayload[] =
checkedSiteAuthority.map((siteId) => ({
siteId,
authrtId: data.roleId,
mode: "C",
}));
try {
await handleSave(data);
if (data.mngrYn === "N") return;
await SiteAuthorityApi.createSiteAuthority(siteAuthorityPayload);
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.SITE_AUTHORITY],
});
} catch (error) {
console.error(error);
}
};
useEffect(() => {
if (!siteAuthorityList?.content.length) return;
const checkedSiteAuthority = siteAuthorityList.content.filter(
(item) => item.mode === "R",
);
setCheckedSiteAuthority(checkedSiteAuthority.map((item) => item.siteId));
}, [siteAuthorityList]);
return (
<FormWrapper form={form} isModifyMode={isModifyMode} {...props}>
<FormWrapper
form={form}
isModifyMode={isModifyMode}
handleSave={saveInterceptor}
{...props}
>
<div className="card">
<Table variant="secondary">
<colgroup>
@@ -165,6 +223,29 @@ function RoleForm({ roleId, form, ...props }: RoleFormProps) {
/>
</TableCell>
</TableRow>
{form.watch("mngrYn") === "Y" && (
<TableRow>
<TableHead data-required>관리사이트</TableHead>
<TableCell className="border">
<div className="flex flex-wrap items-center gap-4">
{siteAuthorityList?.content.map((item) => (
<div
key={item.siteId}
className="flex items-center gap-2"
>
<Checkbox
defaultChecked={item.mode === "R"}
onCheckedChange={(checked: boolean) => {
handleSiteAuthorityChecked(checked, item.siteId);
}}
/>
<span className="whitespace-nowrap">{item.siteNm}</span>
</div>
))}
</div>
</TableCell>
</TableRow>
)}
<TableRow>
<TableHead data-required>사용여부</TableHead>
<TableCell className="border">
+13 -3
View File
@@ -7,11 +7,12 @@ import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import { Button, Input, Label } from "@common/components";
import { RoleApi, type Role, type RoleParams } from "@/entities/role";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import { columns } from "../model/table-columns";
import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
import { createColumns } from "../model/table-columns";
function RolePage() {
const router = useRouter();
const alert = useAlertStore((state) => state.setMessage);
const [keyword, setKeyword] = useState("");
const [searchParams, setSearchParams] = useState<RoleParams>({
@@ -33,6 +34,13 @@ function RolePage() {
}),
});
const { mutateAsync: deleteRole } = useMutationWrapper({
mutationFn: (roleId: string) => RoleApi.deleteRole(roleId),
onSuccess: () => {
alert("삭제되었습니다.", query.refetch);
},
});
const handleRowClick = (row: Role) => {
router.push(`/role/register/${row.roleId}`);
};
@@ -40,7 +48,9 @@ function RolePage() {
const { tableComponent } = useDataTable({
query,
tableOptions: {
columns,
columns: createColumns({
deleteRole,
}),
state: {
pagination,
},
+24
View File
@@ -0,0 +1,24 @@
import type { NodeModel } from "@minoru/react-dnd-treeview";
import type { TreeMenu } from "@/entities/tree-menu";
export const generateMenuTreeData = (
data: TreeMenu[],
): NodeModel<TreeMenu>[] => {
return data.reduce<NodeModel<TreeMenu>[]>((acc, menu) => {
const node: NodeModel<TreeMenu> = {
id: menu.menuCd ?? 0,
parent: menu.parentId ?? 0,
text: menu.menuSubKornNm ?? "",
droppable: true,
data: menu,
};
acc.push(node);
if (menu.children?.length) {
acc.push(...generateMenuTreeData(menu.children));
}
return acc;
}, []);
};
@@ -1,6 +1,32 @@
import dayjs from "dayjs";
import { memo, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { type ColumnDef } from "@tanstack/react-table";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Button,
Checkbox,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@common/components";
import type { Site } from "@/entities/site";
import { TreeMenuApi } from "@/entities/tree-menu";
import {
MenuApi,
siteMapFormSchema,
type SiteMapFormData,
} from "@/entities/menu";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import { MenuTree } from "../ui/menu-tree";
import { generateMenuTreeData } from "../lib/utils";
export const columns: ColumnDef<Site>[] = [
{
@@ -45,4 +71,81 @@ export const columns: ColumnDef<Site>[] = [
return date.format("YYYY-MM-DD");
},
},
{
id: "action",
header: "관리",
cell: ({ row }) => {
return (
<div onClick={(e) => e.stopPropagation()}>
<ActionCell data={row.original} />
</div>
);
},
},
];
const ActionCell = memo(({ data }: { data: Site }) => {
const { confirm } = useConfirm();
const alert = useAlertStore((state) => state.setMessage);
const [open, setOpen] = useState(false);
const { data: menuTreeData, refetch } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, data.siteId],
queryFn: () => TreeMenuApi.getTreeMenuList(data.siteId),
select: (data) => generateMenuTreeData(data),
enabled: !!data.siteId && open,
});
const { mutate: publishSiteMap } = useMutationWrapper({
mutationFn: (data: SiteMapFormData["siteMap"]) =>
MenuApi.publishSiteMap(data),
onSuccess: () => {
alert("사이트맵이 저장되었습니다.", () => {
setOpen(false);
refetch();
});
},
});
const form = useForm<SiteMapFormData>({
resolver: zodResolver(siteMapFormSchema),
});
const fieldArray = useFieldArray({
control: form.control,
name: "siteMap",
});
const handlePublishSiteMap = async () => {
if (await confirm("저장하시겠습니까?")) {
publishSiteMap(form.getValues("siteMap"));
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button size="sm">사이트맵</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{data.siteNm} - 사이트맵 관리</DialogTitle>
</DialogHeader>
<div>
<div className="flex items-center justify-end gap-2 p-2">
<span className="text-xl">표시여부</span>
<Checkbox checked />
</div>
<MenuTree tree={menuTreeData ?? []} fieldArray={fieldArray} />
</div>
<DialogFooter>
<Button onClick={handlePublishSiteMap}>저장</Button>
<DialogClose asChild>
<Button color="white">취소</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
});
ActionCell.displayName = "ActionCell";
@@ -0,0 +1,58 @@
import type { ComponentProps } from "react";
import { Button, Input } from "@common/components";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
type EditableNodeProps = {
depth?: number;
onSave: () => void;
onCancel: () => void;
} & ComponentProps<typeof Input>;
function EditableNode({
depth,
onSave,
onCancel,
...props
}: EditableNodeProps) {
return (
<div
className="flex items-center gap-2 rounded-sm bg-accent p-2"
style={{ marginInlineStart: !depth ? 0 : depth * 5 + 5 }}
onClick={(e) => e.stopPropagation()}
>
<Input {...props} className="h-[2.5rem] !text-[1.3rem] text-foreground" />
<div className="flex items-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-8 !bg-transparent"
onClick={onSave}
aria-label="저장"
>
<i className="diveicon di-check text-2xl" />
</Button>
</TooltipTrigger>
<TooltipContent>저장</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-8 !bg-transparent"
onClick={onCancel}
aria-label="취소"
>
<i className="diveicon di-x" />
</Button>
</TooltipTrigger>
<TooltipContent>취소</TooltipContent>
</Tooltip>
</div>
</div>
);
}
export { EditableNode };
+128
View File
@@ -0,0 +1,128 @@
import { useState } from "react";
import type { NodeModel } from "@minoru/react-dnd-treeview";
import type { UseFieldArrayReturn } from "react-hook-form";
import { Button, Checkbox, cn } from "@common/components";
import { type TreeMenu } from "@/entities/tree-menu";
import type { SiteMapFormData, SiteMapPayloadData } from "@/entities/menu";
import { EditableNode } from "./editable-node";
type MenuNodeProps = {
node: NodeModel<TreeMenu>;
depth: number;
isOpen: boolean;
fieldArray: UseFieldArrayReturn<SiteMapFormData>;
onToggle: (id: NodeModel["id"]) => void;
};
function MenuNode({
node,
depth,
isOpen,
fieldArray,
onToggle,
}: MenuNodeProps) {
const { id, text, data } = node;
const indent = depth * 10 + 5;
const [editMode, setEditMode] = useState(false);
const [nodeName, setNodeName] = useState(text);
const [otptYn, setOtptYn] = useState(data?.otptYn ?? "N");
const updateField = (id: number, newField: SiteMapPayloadData) => {
const existingFieldIndex = fieldArray.fields.findIndex(
(field) => field.menuCd === id,
);
if (existingFieldIndex !== -1) {
fieldArray.update(existingFieldIndex, newField);
} else {
fieldArray.append(newField);
}
};
const handleToggle = () => onToggle(id);
const handleSave = async () => {
const newField = {
menuCd: id as number,
menuSubKornNm: nodeName,
otptYn,
};
updateField(id as number, newField);
setEditMode(false);
};
const handleCancel = () => {
setEditMode(false);
setNodeName(text);
};
const handleOtptYnChange = (checked: boolean) => {
const newField = {
menuCd: id as number,
menuSubKornNm: nodeName,
otptYn: checked ? "Y" : "N",
};
updateField(id as number, newField);
setOtptYn(checked ? "Y" : "N");
};
return (
<div
style={{ paddingInlineStart: indent }}
className={cn(
"group flex cursor-pointer items-center gap-2 rounded-sm p-2 hover:bg-accent",
editMode && "p-0 pr-2 hover:bg-transparent",
)}
onClick={(e) => {
e.stopPropagation();
handleToggle();
}}
>
{editMode ? (
<EditableNode
value={nodeName}
onChange={(e) => setNodeName(e.target.value)}
onSave={handleSave}
onCancel={handleCancel}
/>
) : (
<div className="flex w-full items-center justify-between">
<div className="flex items-center">
{data?.children?.length && (
<i
className={cn(
"diveicon di-arrow-forward mr-2 text-base text-label",
isOpen && "rotate-90",
)}
/>
)}
<span className="text-[1.4rem] font-medium text-label">
{nodeName}
</span>
</div>
<Button
variant="ghost"
size="icon"
className="invisible !bg-transparent group-hover:visible"
onClick={(e) => {
e.stopPropagation();
setEditMode(true);
}}
>
<i className="diveicon di-pencil" />
</Button>
</div>
)}
<Checkbox
className="ml-auto"
checked={otptYn === "Y"}
onClick={(e) => {
e.stopPropagation();
}}
onCheckedChange={handleOtptYnChange}
/>
</div>
);
}
export { MenuNode };
+70
View File
@@ -0,0 +1,70 @@
"use client";
import { useRef } from "react";
import type { UseFieldArrayReturn } from "react-hook-form";
import {
Tree,
type NodeModel,
type TreeMethods,
} from "@minoru/react-dnd-treeview";
import { Button } from "@common/components";
import type { SiteMapFormData } from "@/entities/menu";
import { type TreeMenu } from "@/entities/tree-menu";
import { MenuNode } from "./menu-node";
type MenuTreeProps = {
tree: NodeModel<TreeMenu>[];
fieldArray: UseFieldArrayReturn<SiteMapFormData>;
};
function MenuTree({ tree, fieldArray }: MenuTreeProps) {
const treeMethods = useRef<TreeMethods>(null);
return (
<>
<div className="max-h-[calc(100vh-33.5rem)] min-h-[40vh] overflow-auto">
<Tree
tree={tree}
rootId={0}
ref={treeMethods}
render={(node: NodeModel<TreeMenu>, props) => {
return <MenuNode node={node} fieldArray={fieldArray} {...props} />;
}}
onDrop={() => {}}
canDrag={() => false}
canDrop={() => false}
classes={{
container: "flex flex-col",
dropTarget: "!bg-[rgba(32,148,250,0.5)] rounded-sm",
placeholder: "relative",
}}
sort={false}
/>
</div>
<div className="flex gap-2">
<Button
color="lightGray"
className="flex-1 items-center px-0"
onClick={() => {
treeMethods.current?.openAll();
}}
>
<i className="diveicon di-arrows-angle-expand" />
전체펼침
</Button>
<Button
color="lightGray"
className="flex-1 items-center px-0"
onClick={() => {
treeMethods.current?.closeAll();
}}
>
<i className="diveicon di-arrows-angle-contract" />
전체접기
</Button>
</div>
</>
);
}
export { MenuTree };
@@ -36,9 +36,14 @@ export const columns: ColumnDef<Post>[] = [
accessorKey: "pstgPrdSeNm",
header: "게시기간",
cell: ({ row }) => {
const pstgPrdSeCd = row.original.pstgPrdSeCd;
const pstgPrdSeNm = row.original.pstgPrdSeNm;
const pstgBgngDt = row.original.pstgBgngDt;
const pstgEndDt = row.original.pstgEndDt;
if (pstgPrdSeCd === "99") {
return pstgPrdSeNm;
}
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
},
},
@@ -0,0 +1 @@
export { UserStatisticsApi } from "./user-statistics-service";
@@ -0,0 +1,138 @@
import {
type AccessLog,
type AccessLogParams,
type AccessLogService,
type PageableList,
} from "../model/user-statistics-interface";
// 목 데이터
const mockAccessLogs: AccessLog[] = [
{
no: 1,
accessTime: "2024-01-15 14:30:25",
accessIp: "192.168.1.100",
accessRegion: "서울특별시",
urlPath: "/index",
browser: "Chrome",
site: "SITE1",
accessType: "메인",
},
{
no: 2,
accessTime: "2024-01-15 14:28:15",
accessIp: "203.241.50.123",
accessRegion: "부산광역시",
urlPath: "/board/list",
browser: "Edge",
site: "SITE2",
accessType: "게시판",
},
{
no: 3,
accessTime: "2024-01-15 14:25:42",
accessIp: "175.223.45.67",
accessRegion: "대구광역시",
urlPath: "/statistics",
browser: "Safari",
site: "SITE1",
accessType: "통계",
},
{
no: 4,
accessTime: "2024-01-15 14:22:18",
accessIp: "211.252.87.45",
accessRegion: "인천광역시",
urlPath: "/contents/view",
browser: "Firefox",
site: "SITE3",
accessType: "컨텐츠",
},
{
no: 5,
accessTime: "2024-01-15 14:20:33",
accessIp: "121.254.12.89",
accessRegion: "광주광역시",
urlPath: "/search",
browser: "Chrome",
site: "SITE1",
accessType: "검색",
},
];
// 목 API 서비스
class UserStatisticsApiImpl implements AccessLogService {
async getAccessLogList(
params: AccessLogParams,
): Promise<PageableList<AccessLog>> {
// 실제 API 호출 대신 목 데이터 반환
const { page = 0, size = 10 } = params;
// 필터링 로직 (실제로는 서버에서 처리)
let filteredLogs = [...mockAccessLogs];
if (params.urlPath) {
filteredLogs = filteredLogs.filter((log) =>
log.urlPath.toLowerCase().includes(params.urlPath!.toLowerCase()),
);
}
if (params.accessIp) {
filteredLogs = filteredLogs.filter((log) =>
log.accessIp.includes(params.accessIp!),
);
}
if (params.accessRegion) {
filteredLogs = filteredLogs.filter((log) =>
log.accessRegion.includes(params.accessRegion!),
);
}
if (params.browser) {
filteredLogs = filteredLogs.filter(
(log) => log.browser.toLowerCase() === params.browser!.toLowerCase(),
);
}
if (params.siteId) {
filteredLogs = filteredLogs.filter((log) => log.site === params.siteId);
}
// 페이지네이션
const start = page * size;
const end = start + size;
const paginatedLogs = filteredLogs.slice(start, end);
// 목 응답 생성
return {
content: paginatedLogs,
pageable: {
pageNumber: page,
pageSize: size,
sort: {
empty: true,
sorted: false,
unsorted: true,
},
offset: page * size,
paged: true,
unpaged: false,
},
last: (page + 1) * size >= filteredLogs.length,
totalPages: Math.ceil(filteredLogs.length / size),
totalElements: filteredLogs.length,
size,
number: page,
sort: {
empty: true,
sorted: false,
unsorted: true,
},
first: page === 0,
numberOfElements: paginatedLogs.length,
empty: paginatedLogs.length === 0,
};
}
}
export const UserStatisticsApi = new UserStatisticsApiImpl();
@@ -0,0 +1 @@
export { UserStatisticsPage } from "./ui/user-statistics-page";
@@ -0,0 +1,148 @@
import { type ChartData, type ChartOptions } from "chart.js";
// 일별 접속 현황 차트 데이터
export const monthlyStatisticsData = {
labels: [
"1일",
"2일",
"3일",
"4일",
"5일",
"6일",
"7일",
"8일",
"9일",
"10일",
"11일",
"12일",
"13일",
"14일",
"15일",
],
datasets: [
{
type: "bar" as const,
label: "페이지뷰",
data: [
850, 1000, 750, 750, 700, 750, 780, 800, 850, 900, 1200, 1700, 2300,
2100, 2600,
],
backgroundColor: "rgba(54, 162, 235, 0.8)",
order: 2,
},
{
type: "line" as const,
label: "신규 접속자",
data: [
700, 600, 500, 550, 500, 550, 500, 550, 600, 850, 1200, 1500, 1700,
2300, 2200,
],
borderColor: "rgba(255, 99, 132, 0.8)",
borderWidth: 2,
tension: 0.4,
pointRadius: 3,
order: 0,
},
{
type: "line" as const,
label: "총 접속자",
data: [
200, 300, 400, 500, 600, 500, 550, 500, 400, 500, 350, 800, 900, 1200,
1300,
],
borderColor: "rgba(75, 192, 192, 0.8)",
borderWidth: 2,
tension: 0.4,
pointRadius: 3,
order: 1,
},
],
};
export const monthlyStatisticsOptions: ChartOptions<"bar"> = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "top" as const,
},
tooltip: {
mode: "index" as const,
intersect: false,
},
},
};
// 사이트별 접속자 차트 데이터
export const siteVisitorsData: ChartData<"bar"> = {
labels: ["SITE1", "SITE2", "SITE3", "SITE4", "SITE5"],
datasets: [
{
label: "사이트별 접속자",
data: [2500, 2000, 1500, 1200, 800],
backgroundColor: [
"rgba(54, 162, 235, 0.8)",
"rgba(153, 102, 255, 0.8)",
"rgba(255, 99, 132, 0.8)",
"rgba(255, 159, 64, 0.8)",
"rgba(255, 205, 86, 0.8)",
],
borderWidth: 1,
},
],
};
export const siteVisitorsOptions: ChartOptions<"bar"> = {
responsive: true,
maintainAspectRatio: false,
indexAxis: "y" as const,
plugins: {
legend: {
display: false,
},
},
};
// 브라우저별 접속자 차트 데이터
export const browserStatisticsData: ChartData<"doughnut"> = {
labels: ["Chrome", "Edge", "Safari", "Firefox", "Internet Explorer"],
datasets: [
{
data: [45, 25, 15, 10, 5],
backgroundColor: [
"rgba(54, 162, 235, 0.8)",
"rgba(153, 102, 255, 0.8)",
"rgba(255, 99, 132, 0.8)",
"rgba(255, 159, 64, 0.8)",
"rgba(255, 205, 86, 0.8)",
],
borderColor: [
"rgba(54, 162, 235, 1)",
"rgba(153, 102, 255, 1)",
"rgba(255, 99, 132, 1)",
"rgba(255, 159, 64, 1)",
"rgba(255, 205, 86, 1)",
],
borderWidth: 1,
},
],
};
export const browserStatisticsOptions: ChartOptions<"doughnut"> = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "bottom" as const,
},
},
};
export const chartData = {
monthlyStatisticsData,
monthlyStatisticsOptions,
siteVisitorsData,
siteVisitorsOptions,
browserStatisticsData,
browserStatisticsOptions,
};
@@ -0,0 +1,39 @@
import { createColumnHelper } from "@tanstack/react-table";
import { type AccessLog } from "./user-statistics-interface";
const columnHelper = createColumnHelper<AccessLog>();
export const columns = [
columnHelper.accessor("no", {
header: "No",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("accessTime", {
header: "접속일시",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("accessIp", {
header: "접속IP",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("accessRegion", {
header: "접속지역",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("urlPath", {
header: "URL경로",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("browser", {
header: "브라우저",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("site", {
header: "사이트",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("accessType", {
header: "메뉴명",
cell: (info) => info.getValue(),
}),
];
@@ -0,0 +1,56 @@
export interface AccessLog {
no: number;
accessTime: string;
accessIp: string;
accessRegion: string;
urlPath: string;
browser: string;
site: string;
accessType: string;
}
export interface AccessLogParams {
page?: number;
size?: number;
urlPath?: string;
accessIp?: string;
accessRegion?: string;
browser?: string;
accessDateStart?: string;
accessDateEnd?: string;
siteId?: string;
sort?: string;
}
export interface AccessLogService {
getAccessLogList(params: AccessLogParams): Promise<PageableList<AccessLog>>;
}
export interface PageableList<T> {
content: T[];
pageable: {
pageNumber: number;
pageSize: number;
sort: {
empty: boolean;
sorted: boolean;
unsorted: boolean;
};
offset: number;
paged: boolean;
unpaged: boolean;
};
last: boolean;
totalPages: number;
totalElements: number;
size: number;
number: number;
sort: {
empty: boolean;
sorted: boolean;
unsorted: boolean;
};
first: boolean;
numberOfElements: number;
empty: boolean;
}
@@ -0,0 +1,324 @@
"use client";
import dayjs from "dayjs";
import { useState } from "react";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
ArcElement,
Filler,
Legend,
Tooltip,
type ChartData,
} from "chart.js";
import { Bar, Doughnut } from "react-chartjs-2";
import {
Button,
Input,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@common/components";
import { SiteSelect } from "@/entities/site";
import { useDataTable } from "@/shared/lib";
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
import { UserStatisticsApi } from "../api";
import { chartData } from "../model/chart-data";
import { columns } from "../model/table-columns";
import { type AccessLogParams } from "../model/user-statistics-interface";
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
ArcElement,
Filler,
Legend,
Tooltip,
);
type DateRange = {
from?: Date | undefined;
to?: Date | undefined;
};
function UserStatisticsPage() {
const defaultDateRange: DateRange = {
from: dayjs().subtract(7, "day").toDate(),
to: dayjs().toDate(),
};
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [keywordType, setKeywordType] = useState("");
const [keyword, setKeyword] = useState("");
const [presetDateRange, setPresetDateRange] = useState("1W");
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
const [searchParams, setSearchParams] = useState<AccessLogParams>({
urlPath: "",
accessIp: "",
accessRegion: "",
browser: "",
accessDateStart: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
accessDateEnd: dayjs().format("YYYY-MM-DD"),
sort: "accessTime,desc",
});
const query = useQuery({
queryKey: ["user-statistics", searchParams, pagination],
queryFn: () =>
UserStatisticsApi.getAccessLogList({
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
placeholderData: keepPreviousData,
});
const { tableComponent, tableInstance: table } = useDataTable({
query,
tableOptions: {
columns,
state: {
pagination,
},
manualPagination: true,
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
},
dataTableOptions: {
viewPagination: true,
},
});
const handlePresetDateRange = (value: string) => {
let presetDateRange: DateRange;
switch (value) {
case "1W":
presetDateRange = {
from: dayjs().subtract(7, "day").toDate(),
to: dayjs().toDate(),
};
break;
case "1M":
presetDateRange = {
from: dayjs().subtract(30, "day").toDate(),
to: dayjs().toDate(),
};
break;
case "3M":
presetDateRange = {
from: dayjs().subtract(90, "day").toDate(),
to: dayjs().toDate(),
};
break;
default:
presetDateRange = defaultDateRange;
}
setPresetDateRange(value);
setDateRange(presetDateRange);
};
const handleDateRangeChange = (dateRange: DateRange) => {
setDateRange(dateRange);
setPresetDateRange("");
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setPagination({
...pagination,
pageIndex: 0,
});
// 검색 조건에 따라 검색 파라미터 설정
const params: AccessLogParams = {
sort: "accessTime,desc",
accessDateStart: dateRange.from
? dayjs(dateRange.from).format("YYYY-MM-DD")
: "",
accessDateEnd: dateRange.to
? dayjs(dateRange.to).format("YYYY-MM-DD")
: "",
};
// 검색어 타입에 따라 파라미터 설정
switch (keywordType) {
case "urlPath":
params.urlPath = keyword;
break;
case "ip":
params.accessIp = keyword;
break;
case "region":
params.accessRegion = keyword;
break;
case "browser":
params.browser = keyword;
break;
}
setSearchParams(params);
};
const handleReset = () => {
setKeywordType("");
setKeyword("");
setDateRange(defaultDateRange);
setPresetDateRange("1W");
setSearchParams({
urlPath: "",
accessIp: "",
accessRegion: "",
browser: "",
accessDateStart: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
accessDateEnd: dayjs().format("YYYY-MM-DD"),
sort: "accessTime,desc",
});
};
return (
<div className="card card-border !mt-6">
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2">날짜</Label>
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1W">일주일</SelectItem>
<SelectItem value="1M">1개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem>
</SelectContent>
</Select>
<InputDateRangePicker
dateRange={dateRange}
setDateRange={handleDateRangeChange}
/>
<Label className="ml-4 mr-2 text-label">사이트</Label>
<SiteSelect enabledAllSite className="w-[16rem]" />
<Label className="ml-4 mr-2 text-label">검색</Label>
<Select
value={keywordType}
onValueChange={(value) => setKeywordType(value)}
>
<SelectTrigger className="h-[3.2rem] w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="urlPath">URL경로</SelectItem>
<SelectItem value="ip">접속IP</SelectItem>
<SelectItem value="region">접속지역</SelectItem>
<SelectItem value="browser">브라우저</SelectItem>
</SelectContent>
</Select>
<Input
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
{/* 차트 영역 */}
<div className="grid grid-cols-2">
{/* 일별 접속 현황 */}
<div className="card card-border">
<div className="card-header">
<h3 className="text-xl font-medium">일별 접속 현황</h3>
</div>
<div className="h-96 p-4">
<Bar
data={chartData.monthlyStatisticsData as ChartData<"bar">}
options={chartData.monthlyStatisticsOptions}
/>
</div>
</div>
<div className="grid grid-cols-2">
{/* 사이트별 접속자 */}
<div className="card card-border">
<div className="card-header">
<h3 className="text-xl font-medium">사이트별 접속자</h3>
</div>
<div className="h-96 p-4">
<Bar
data={chartData.siteVisitorsData}
options={chartData.siteVisitorsOptions}
/>
</div>
</div>
{/* 브라우저별 접속자 */}
<div className="card card-border">
<div className="card-header">
<h3 className="text-xl font-medium">브라우저별 접속자</h3>
</div>
<div className="h-96 p-4">
<Doughnut
data={chartData.browserStatisticsData}
options={chartData.browserStatisticsOptions}
/>
</div>
</div>
</div>
</div>
{/* 접속 로그 상세 */}
<div className="card card-border">
<div className="card-header">
<div className="flex w-full items-center justify-between">
<h3 className="text-2xl font-medium text-label">접속 로그 상세</h3>
<div className="flex gap-2">
<Select
value={table.getState().pagination.pageSize.toString()}
onValueChange={(value) => {
table.setPageSize(parseInt(value));
}}
>
<SelectTrigger className="h-[3.2rem] w-[12rem]">
<SelectValue placeholder="페이지 크기" />
</SelectTrigger>
<SelectContent>
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={pageSize.toString()}>
{pageSize}개씩 보기
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
</div>
</div>
);
}
export { UserStatisticsPage };
@@ -1,6 +1,11 @@
import dayjs from "dayjs";
import type { ColumnDef } from "@tanstack/react-table";
import type { UserListResponse } from "@/entities/user";
import {
USER_STATE_CODE,
type UserListResponse,
type UserStateCode,
} from "@/entities/user";
import { Badge } from "@common/components";
export const columns: ColumnDef<UserListResponse>[] = [
{
@@ -42,8 +47,11 @@ export const columns: ColumnDef<UserListResponse>[] = [
header: "기관/부서",
},
{
accessorKey: "userStateCodeName",
accessorKey: "userStateCode",
header: "회원상태",
cell: ({ row }) => {
return getUserStateBadge(row.original.userStateCode);
},
},
{
accessorKey: "aprvYmd",
@@ -63,3 +71,18 @@ export const columns: ColumnDef<UserListResponse>[] = [
},
},
];
const getUserStateBadge = (userStateCode: UserStateCode) => {
switch (userStateCode) {
case USER_STATE_CODE.대기:
return <Badge color="gray">대기</Badge>;
case USER_STATE_CODE.정상:
return <Badge color="green">정상</Badge>;
case USER_STATE_CODE.정지:
return <Badge color="yellow">정지</Badge>;
case USER_STATE_CODE.탈퇴:
return <Badge color="purple">탈퇴</Badge>;
case USER_STATE_CODE.삭제:
return <Badge color="red">삭제</Badge>;
}
};
@@ -16,20 +16,11 @@ function CreatePage() {
defaultValues: {
userId: "",
userName: "",
email: "",
password: "",
pswdHnt: "",
pswdAns: "",
mphn: "",
telno: "",
zip: "",
addr: "",
daddr: "",
laeSeCd: "",
ogdpSeCd: "",
expryYmd: "",
instSeCd: "",
enoCd: "",
},
});
+37 -26
View File
@@ -8,7 +8,6 @@ import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import {
Button,
Checkbox,
DateRangePicker,
Input,
Label,
Select,
@@ -25,10 +24,11 @@ import {
} from "@/entities/user";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
import { columns } from "../model/table-columns";
type DateRange = {
from: Date | undefined;
from?: Date | undefined;
to?: Date | undefined;
};
@@ -39,7 +39,8 @@ function UsersPage() {
pageIndex: 0,
pageSize: 10,
});
const [dateRange, setDateRange] = useState<DateRange | undefined>({
const [presetDateRange, setPresetDateRange] = useState("");
const [dateRange, setDateRange] = useState<DateRange>({
from: undefined,
to: undefined,
});
@@ -61,7 +62,7 @@ function UsersPage() {
});
const { data: userSttsStats } = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS],
queryKey: [ENDPOINT.USER_SERVICE.USERS, "userStatusStatistics"],
queryFn: () => UserApi.selectUserSttsStats(),
});
@@ -144,7 +145,7 @@ function UsersPage() {
};
const handlePresetDateRange = (value: string) => {
let presetDateRange: DateRange | undefined;
let presetDateRange: DateRange;
switch (value) {
case "1M":
presetDateRange = {
@@ -165,14 +166,24 @@ function UsersPage() {
};
break;
default:
presetDateRange = undefined;
presetDateRange = {
from: undefined,
to: undefined,
};
}
setPresetDateRange(value);
setDateRange(presetDateRange);
};
const getUserSttsStatsCnt = (code: string): number => {
if (!Array.isArray(userSttsStats)) return 0;
return userSttsStats.find((item) => item.sttsCd === code)?.cnt ?? 0;
const handleDateRangeChange = (dateRange: DateRange) => {
setDateRange(dateRange);
setPresetDateRange("");
};
const getUserSttsStatsCnt = (code: string): string => {
if (!Array.isArray(userSttsStats)) return "0";
const count = userSttsStats.find((item) => item.sttsCd === code)?.cnt ?? 0;
return count.toLocaleString();
};
const handleUserStateCodeChange = (code: string, checked: boolean) => {
@@ -277,23 +288,23 @@ function UsersPage() {
</div>
<div className="flex items-center gap-2">
<Label className="mr-2 text-label">기간</Label>
<DateRangePicker value={dateRange} onValueChange={setDateRange}>
<div className="card card-border flex items-center gap-2">
<Select onValueChange={handlePresetDateRange}>
<SelectTrigger>
<SelectValue
placeholder="기간 선택"
className="w-[24rem]"
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="1M">1개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem>
<SelectItem value="6M">6개월</SelectItem>
</SelectContent>
</Select>
</div>
</DateRangePicker>
<Select
value={presetDateRange}
onValueChange={handlePresetDateRange}
>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1M">1개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem>
<SelectItem value="6M">6개월</SelectItem>
</SelectContent>
</Select>
<InputDateRangePicker
dateRange={dateRange}
setDateRange={handleDateRangeChange}
/>
</div>
</div>
<div className="flex items-center gap-2">
+2
View File
@@ -23,6 +23,7 @@ export const ENDPOINT = {
PRIVACYLOG: "/user-service//api/v1/prvc-inq-logs",
},
CMS_SERVICE: {
DASHBOARD: "/cms-service/api/v1/dashboard",
SITES: "/cms-service/api/v1/sites",
IPS: "/cms-service/api/v1/site-ips",
DOMAINS: "/cms-service/api/v1/site-dmns",
@@ -53,6 +54,7 @@ export const ENDPOINT = {
GROUP_USERS: "/cms-service/api/v1/group-users",
MANAGER_IP: "/cms-service/api/v1/mgrip",
APILOG: "/cms-service/api/v1/api-logs",
SITE_AUTHORITY: "/cms-service/api/v1/site-authrt",
},
BOARD_SERVICE: {
BBS: "/board-service/api/v1/bbs",

Some files were not shown because too many files have changed in this diff Show More