Compare commits

...
Author SHA1 Message Date
JIeunhuh 4f461fe419 날짜 포맷 2025-05-20 15:13:53 +09:00
JIeunhuh 19cdae39bd 데이터 조회 기능 공통함수로 추가 2025-05-20 13:36:44 +09:00
JIeunhuh 8932752b84 Merge commit '7c7cc47d20c3a7b440ba3bb576407605b29233ca' into iipbs/sn/oi 2025-05-20 09:34:20 +09:00
JIeunhuh cc105e9666 침수예방 데이터 조회 기능 추가 2025-05-19 18:02:55 +09:00
JIeunhuh 38537d9a11 modal 변경 및 상세팝업 입력정보 api 작업 우선 완료(추후 레이어 추가 시 확인필요)
iipbs_sn / build (push) Waiting to run
2025-05-19 14:29:17 +09:00
YangJinSoo94 7c7cc47d20 -chart yFieldText2 추가(라인바는 2개)
-대시보드 수정
2025-05-19 09:52:14 +09:00
YangJinSoo94 c2a2ca8baf -메인그래프 강우량-bod, 유입방출TP 완료, -공통 차트 커스텀 진행중 2025-05-16 17:38:23 +09:00
swcho ecab665c2e Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs/sn/search
iipbs_sn / build (push) Has been cancelled
2025-05-16 16:46:46 +09:00
swcho d11343726f feat: 연도 범위 선택 기능 추가 및 SearchModal 데이터 처리 개선 2025-05-16 16:46:06 +09:00
JIeunhuh 9029bfaf64 api변경
iipbs_sn / build (push) Waiting to run
2025-05-16 16:41:28 +09:00
JIeunhuh bbc48d5aef Merge commit '9cf8a5355c357adb4e3e995d3c1ab0301943fec3' into iipbs/sn/dev
iipbs_sn / build (push) Waiting to run
2025-05-16 16:33:05 +09:00
JIeunhuh 9cf8a5355c 하수관로 차트 / 사용자 입력 정보 api 요청 작업 중 2025-05-16 16:32:02 +09:00
YangJinSoo94 7678c722c4 라인바, 바차트 class 수정 2025-05-16 14:17:34 +09:00
JIeunhuh 6315c5406b 불필요한 파일 모두 삭제 2025-05-15 18:07:55 +09:00
YangJinSoo94 d199166e27 바차트, 라인차트 툴팁 커스텀화 진행(글씨크기, 위치, 사이즈 등) 2025-05-15 16:53:31 +09:00
YangJinSoo94 8c5f0a9895 barChart 범례 하단에 위치 및 수정 2025-05-15 13:40:57 +09:00
yschoi 3f447a0096 버튼 z-index 수정
iipbs_sn / build (push) Waiting to run
2025-05-15 13:25:56 +09:00
JIeunhuh edd52f5336 테이블 안보여서 주석처리 2025-05-15 11:34:52 +09:00
JIeunhuh d2cf58dc4b 하수관로 처트 팝업 구현 2025-05-15 09:59:37 +09:00
swcho 7a9038bbe2 feat, fix : SidoSigunguSelectBox 및 YearRangeSelectBox, SelectBox2 컴포넌트 추가, SearchModal.tsx 스타일 수정
- 선택한 시도에 따라 동적으로 시군구 옵션이 바뀌는 SidoSigunguSelectBox 구현
- 시작/종료년도 유효성 검증이 포함된 YearRangeSelectBox 구현
- 드롭다운 z-index 개선 및 재사용 가능한 SelectBox2 컴포넌트 도입
- SearchModal.tsx 태그들 공통 컴포넌트로 변환하여 스타일 통일
2025-05-14 17:53:38 +09:00
swcho 037a04f1c9 fix : 테두리 곡률 높임, BASIC_TYPES key -> val, label -> text로 수정 2025-05-14 13:18:04 +09:00
JIeunhuh 726154dd79 변경된 api 개발서버에 반영안되서 우선 이전버전으로 변경
iipbs_sn / build (push) Has been cancelled
2025-05-14 09:32:31 +09:00
JIeunhuh 6b8b091c1b 상세팝업 하수처리시설 바라보도록 변경
iipbs_sn / build (push) Waiting to run
2025-05-14 09:22:31 +09:00
swcho a8c3e49226 Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs/sn/search
iipbs_sn / build (push) Waiting to run
2025-05-13 18:22:13 +09:00
swcho 44fedd4a60 feat: Add component mapping for 하수처리구역 in layerContentMap 2025-05-13 18:18:43 +09:00
swcho 234954a929 feat, fix : 하수관로, 운영정보, 하수도정비 기본계획 보고서 테이블 헤더 리스트 작성, FeatureClickEventPopup 로그 삭제, UITab2 border 삭제 2025-05-13 18:07:21 +09:00
JIeunhuh ab1ebac157 Merge commit 'c625101e7f3e50a455e27b083fe42e0b8a888f4f' into iipbs/sn/oi
iipbs_sn / build (push) Waiting to run
2025-05-13 17:59:38 +09:00
JIeunhuh dcfafbc8fc 페이지 수정 요청사항 반영 2025-05-13 17:57:27 +09:00
JIeunhuh 9b406aac3d 수정사항 변경 2025-05-13 17:34:19 +09:00
swcho a56712a8a6 feat, fix: FeatureClickEventPopup에서 featureID 가져오기 및 레이어 이름 선택 처리 추가, 탭 기능을 위한 UITab2 컴포넌트 추가 2025-05-13 15:52:59 +09:00
YangJinSoo94 4bf62a3ee3 바차트 수정완료 - 색상 및 상세 디자인 수정예정 2025-05-13 11:08:29 +09:00
leedj312 caed0e6aa1 엑셀다운로드 2개이상 시트 함수 추가 2025-05-13 09:33:29 +09:00
yschoi c625101e7f 맵 중복 생성 수정
iipbs_sn / build (push) Waiting to run
2025-05-12 19:03:33 +09:00
JIeunhuh 1f6eab3fb2 index 링크 추가
iipbs_sn / build (push) Waiting to run
2025-05-12 16:52:30 +09:00
JIeunhuh 105c9de31b 레이어별 컴포넌트 추가 및 저장 버튼 클릭 시 이벤트 처리 추후 작업필요 2025-05-12 14:48:07 +09:00
swcho cec2ebb8c0 feat, fix : SearchModal 및 GisTab에서 onFeatureSelect prop 추가 및 관련 상태 관리 개선 2025-05-12 14:04:24 +09:00
yschoi a64e6d8cd8 Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs-sn-map
iipbs_sn / build (push) Has been cancelled
2025-05-09 17:29:01 +09:00
YangJinSoo94 1d4da4caf5 테이블 boldArray property 추가 (역할 : 지정된 array의 글씨색 굵게 변경) 2025-05-09 16:55:09 +09:00
JIeunhuh bd1c758caa 레이어 컴포넌트 추가
iipbs_sn / build (push) Waiting to run
2025-05-09 16:32:07 +09:00
JIeunhuh e8c293a43b 파일 위치변경 2025-05-09 16:10:08 +09:00
JIeunhuh 13f69f545f Merge commit '4fe52f39a7be61bcf794db3ce0229113d68667ff' into iipbs/sn/dev
iipbs_sn / build (push) Waiting to run
2025-05-09 15:53:30 +09:00
JIeunhuh 4fe52f39a7 모달 최대높이 설정 2025-05-09 15:52:00 +09:00
JIeunhuh 734dd2a1b5 필요없는 코드 정리 및 모달 컴포넌트 추가 2025-05-09 15:49:38 +09:00
JIeunhuh 464def34b6 컴포넌트 분리 시켜 동적으로 렌더링되도록 작업 및 레이어 별 컴포넌트 추가 2025-05-09 15:48:37 +09:00
swcho 8b7d1a64a0 Merge branch 'iipbs/sn/dev' into iipbs/sn/search
iipbs_sn / build (push) Waiting to run
2025-05-09 15:40:47 +09:00
swcho 92ddd91331 feat, fix : 하수관로-하수처리시설 모달과 상세조회 이벤트 처리, 백엔드 페이징 처리 없는 테이블 생성, SearchModal.tsx 파일 위치 이동 (sn/app/map) 2025-05-09 15:38:19 +09:00
swcho 5f407b93bf feat: 하수관로- 하수처리시설, 하수처리구역 조회 기능 구현 2025-05-09 15:28:00 +09:00
JIeunhuh b87a27dfc9 레이어 선택별로 개별 컴포넌트로 동적 처리하도록 분리 2025-05-09 13:11:01 +09:00
JIeunhuh 91771faffa 선택된 레이어에 맞춰 테이블 변경되도록 수정 2025-05-07 18:23:40 +09:00
JIeunhuh 09673d144e api 작업중 2025-05-07 16:57:34 +09:00
yschoi 13414567bb Merge remote-tracking branch 'origin/iipbs/sn/oi' into iipbs-sn-map 2025-05-07 16:09:51 +09:00
JIeunhuh aa456f68d7 api 작업진행중 2025-05-07 16:08:07 +09:00
JIeunhuh e261d980cc Merge commit 'c9ac49d7578d157fda183979b5a6110d4c9a6abe' into iipbs/sn/oi 2025-05-07 11:00:50 +09:00
JIeunhuh b8015ea1d9 하수관망도 피쳐 클릭이벤트 처리 2025-05-07 10:56:45 +09:00
leedj312 f04a463aeb 테이블에서 visible 및 filter처리 2025-05-06 23:09:34 +09:00
yschoi c9ac49d757 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev
iipbs_sn / build (push) Has been cancelled
2025-05-02 17:55:57 +09:00
yschoi 12018e12bf Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs-sn-map 2025-05-02 17:51:45 +09:00
yschoi 20fa32cc58 시설 상세 2025-05-02 17:36:23 +09:00
swcho 2aa31be1dc Gis Search 모달 작업(추가 작업 필요)
iipbs_sn / build (push) Waiting to run
2025-05-02 17:32:58 +09:00
leedj312 826b49d72a 엑셀 to json 0번째 시트만 바라보게 수정 2025-05-02 16:19:36 +09:00
yschoi e443c93061 Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs-sn-map 2025-05-02 16:06:51 +09:00
yschoi 9d77ff2fd2 피쳐 정보 반환 2025-05-02 15:20:55 +09:00
truthwing 370a1076b0 excel_download 폴더명 변경. 2025-04-30 16:08:22 +09:00
truthwing 6447afd162 excel populate 추가 및 상단 range에 헤더 스타일 입히는 작업 완료. 2025-04-30 15:59:05 +09:00
yschoi 589b31d3cb Merge remote-tracking branch 'origin/iipbs/common/main' into iipbs-sn-map 2025-04-30 14:25:17 +09:00
JIeunhuh 24d4f2d326 Merge commit '525307d1ee392efef669de8f912620e6158d142b' into iipbs/sn/dev
iipbs_sn / build (push) Has been cancelled
2025-04-30 10:49:56 +09:00
JIeunhuh 525307d1ee 피쳐 클릭시 모달 오픈 하고 데이터 나오도록 작업(변경필요) 2025-04-30 10:49:07 +09:00
leedj312 235f4bdb29 파일업로드 인터페이스 추가 and porps 방식변경 2025-04-29 19:43:14 +09:00
JIeunhuh 11fd638b19 하수관로 정보팝업 구현 2025-04-29 11:07:31 +09:00
truthwing daba76721e 엑셀 blob 구조 수정 2025-04-29 10:54:29 +09:00
leedj312 2bf254eb3c excel 파일이름 파라미터로 넣기 2025-04-29 10:27:44 +09:00
truthwing 97235d45cd 엑셀버튼 이름 변경할 수 있도록 수정. 2025-04-29 10:06:13 +09:00
JIeunhuh f84cce9c7d test
iipbs_sn / build (push) Has been cancelled
2025-04-28 16:39:46 +09:00
JIeunhuh 611a06faae 테스트
iipbs_sn / build (push) Waiting to run
2025-04-28 16:10:13 +09:00
JIeunhuh f95b093d81 테스트
iipbs_sn / build (push) Waiting to run
2025-04-28 16:01:29 +09:00
JIeunhuh bdae637024 Merge commit '939c1921cf5f72722646430dfa106fdbd7ba47fa' into iipbs/sn/oi 2025-04-28 15:36:09 +09:00
JIeunhuh fb509cffca Merge commit '81afc9f3413a3dd92515566c0e1bf361274e911d' into iipbs/sn/oi 2025-04-28 09:34:28 +09:00
JIeunhuh 939c1921cf 테스트
iipbs_sn / build (push) Has been cancelled
2025-04-25 18:29:56 +09:00
JIeunhuh fb0d462c0f test
iipbs_sn / build (push) Waiting to run
2025-04-25 16:46:17 +09:00
yschoi 81afc9f341 탭 컴포넌트 분리/디자인 수정정 2025-04-25 16:12:09 +09:00
JIeunhuh 0556f73c76 ii분석 페이지 설계 변경토대로 화면재구현
iipbs_sn / build (push) Waiting to run
2025-04-25 14:29:13 +09:00
truthwing 0ba499edc2 json 데이터 받아서 excel에 넣는 작업까지 완료. 2025-04-25 13:36:17 +09:00
yschoi f9ea0d36ce 하이라이트 추가 2025-04-24 20:57:56 +09:00
truthwing d534bc4373 buffer 받아서 임시적으로 내부 수정하는 부분 적용. 2025-04-24 19:56:49 +09:00
truthwing 114b761d50 파일 직접 받아서 처리하는 함수 추가. 2025-04-24 19:44:57 +09:00
truthwing 83641602c6 Merge commit '33cef5eb723bb2450866999436eac71baac0cfa0' into iipbs/common/main 2025-04-24 17:58:19 +09:00
truthwing cb546b8cfa promise 처리 완료. 2025-04-24 15:46:31 +09:00
truthwing 31aa0e078a 엑셀 받으면 json 리턴 하도록 수정. 2025-04-24 15:31:22 +09:00
truthwing fb307527b6 기본값 0으로 설정. 2025-04-24 15:28:37 +09:00
truthwing 3de10413cf 헤더 번호 추가하도록 수정. 2025-04-24 15:27:51 +09:00
YangJinSoo94 636337ac46 테이블 children 오류 수정 2025-04-24 15:24:52 +09:00
YangJinSoo94 459c3723ae Merge branch 'iipbs/common/main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/common/main 2025-04-24 15:06:03 +09:00
YangJinSoo94 30d5d5c9e7 header span 중복으로 생성되는 문제 해결 2025-04-24 15:05:55 +09:00
truthwing f115999101 엑셀 json으로 변경하는 함수 추가. 2025-04-24 14:53:19 +09:00
truthwing 7e9fb08e97 children 안 넣는 경우도 나오도록 수정. 2025-04-24 14:40:53 +09:00
YangJinSoo94 a4310ff090 childeren 복구 2025-04-24 13:18:20 +09:00
JIeunhuh 8373e16d57 Merge commit 'fb5eed6be83fa73302c0c3c57150fa07c2f7fb97' into iipbs/sn/oi 2025-04-24 13:04:48 +09:00
truthwing 331f90d565 테이블 헤더 구조 수정.
searchbaseform 초기화 버튼 클릭시 이벤트 동작되도록 수정.
2025-04-24 12:33:43 +09:00
truthwing 2af1857055 테이블 생성시 키 값 생성 부분 수정. 2025-04-24 12:18:09 +09:00
YangJinSoo94 cfb91d5185 firstcommit 2025-04-24 11:50:57 +09:00
yschoi 1bc19cc8eb Merge remote-tracking branch 'origin/main' into iipbs/sn/dev 2025-04-24 11:18:55 +09:00
yschoi fb5eed6be8 관망망
iipbs_sn / build (push) Waiting to run
2025-04-24 11:01:35 +09:00
yschoi f3c6d538aa 관망도 2025-04-24 10:44:42 +09:00
yschoi 38c75ca92c Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs/sn/map 2025-04-24 10:32:15 +09:00
yschoi 4ec8b6a24c 관망도 추가가 2025-04-24 10:26:49 +09:00
JIeunhuh a88a68c212 화면 수정사항 반영 2025-04-24 10:16:23 +09:00
truthwing 3bff513179 초기화 버큰 클릭 이벤트 추가 및 환경청 정보를 시도 시군구에 추가. 2025-04-23 17:15:00 +09:00
JIeunhuh f5c33e80ae 페이지 오타 수정
iipbs_sn / build (push) Waiting to run
2025-04-23 11:10:41 +09:00
JIeunhuh 3c7dad233f test
iipbs_sn / build (push) Waiting to run
2025-04-23 10:59:00 +09:00
JIeunhuh 4a51017703 test
iipbs_sn / build (push) Waiting to run
2025-04-23 10:52:08 +09:00
JIeunhuh 6632636e5a sn ci/cd
iipbs_sn / build (push) Waiting to run
2025-04-23 10:47:40 +09:00
JIeunhuh 4f34a8ffa0 sn ci/cd 2025-04-23 10:44:54 +09:00
truthwing 75aeadd69d SearchBaseForm ref 연결 완료. 2025-04-22 17:57:45 +09:00
truthwing 675a3583b7 엑셀 클릭 이벤트도 추가. 2025-04-22 17:30:23 +09:00
truthwing 6debee6b57 search base form 추가(사용법은 메신저로 우선 전달) 2025-04-22 17:19:34 +09:00
JIeunhuh db6c3620c2 폼 형식 컴포넌트화 2025-04-22 13:21:52 +09:00
JIeunhuh 893dc8502a 버튼 위치 변경 2025-04-22 11:23:02 +09:00
JIeunhuh d3e114be3a Merge commit '84147da3c5f1f5505aa389ee31540c2eed9451ee' into iipbs/sn/oi 2025-04-22 10:07:36 +09:00
JIeunhuh 1bb6ae08d2 페이지 레이아웃 조정 2025-04-22 09:36:37 +09:00
JIeunhuh 5f666eb4ed 페이지 디자인 변경 2025-04-21 17:15:56 +09:00
YangJinSoo94 84147da3c5 md 파일 업데이트 2025-04-21 16:24:55 +09:00
JIeunhuh 4233e86dbb 메인 페이지에 링크 연결 2025-04-21 15:17:58 +09:00
YangJinSoo94 b9ee38a907 검색영역 초기화 오류 수정 2025-04-21 15:06:47 +09:00
YangJinSoo94 fa46ad6784 검색영역 초기화 오류 개선 2025-04-21 15:04:34 +09:00
JIeunhuh 5e27300f14 i/i 분석 페이지 레이아웃 구현만 우선완료 2025-04-21 14:27:12 +09:00
YangJinSoo94 32b68df7f3 엑셀 다운로드 business_compo 생성 2025-04-21 14:24:02 +09:00
YangJinSoo94 0ff24d8eaf 공통검색단 영역 수정 2025-04-21 13:58:31 +09:00
JIeunhuh d61962996c 공통 레이아웃 컴포넌트로 등록 후 페이지 구현(i/i 분석 >> 기초정보 설정) 2025-04-21 13:29:07 +09:00
YangJinSoo94 eeea80f712 searchformleft md v1 2025-04-18 17:40:20 +09:00
YangJinSoo94 4a6681fe3a 검색영역 초기화가 안되는 문제로 재배포 2025-04-18 17:01:15 +09:00
JIeunhuh e79348ea11 조회 박스 추가 2025-04-18 16:17:26 +09:00
JIeunhuh 4e0fef85f3 공통 레이아웃 작업 및 모의정보설정/관망도 등록화면 작업중 2025-04-18 16:06:52 +09:00
YangJinSoo94 67b3ba8788 헤더 타입이 number일 경우 우측정렬 및 세자리수 끊어주는 거 개발 2025-04-17 16:56:22 +09:00
chanii8 33cef5eb72 Merge branch 'iipbs_cs' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-04-17 15:07:41 +09:00
chanii8 51a97b4179 feat: 모듈 API 권한 허용 컨펌 메시지 수정 2025-04-17 15:07:13 +09:00
chanii8 524d08b620 feat: 모듈 API 권한 관리 작업 2025-04-17 14:10:47 +09:00
chanii8 b05d407304 feat: 파일관리 페이지 작업 2025-04-17 13:28:24 +09:00
truthwing dd4a4e1ec8 전부다 외부 버튼인 경우도 처리. 2025-04-17 13:08:51 +09:00
truthwing 00548eb914 search 폼 형태 수정. 2025-04-17 11:35:35 +09:00
chanii8 9fb4f6aad9 feat: 모듈 API 권한 관리 작업 2025-04-17 11:22:16 +09:00
YangJinSoo94 c3d2804471 searchform left md파일 작성 (초안) 2025-04-17 10:46:40 +09:00
YangJinSoo94 d2beda89c8 searchFormLeft 실수로 더 빠진 프로퍼티 다시 추가 2025-04-17 10:14:34 +09:00
YangJinSoo94 68f4734f3b searchFormLeft 필요없는 프로퍼티 삭제 2025-04-17 10:11:42 +09:00
YangJinSoo94 2e1601b87d searchFormLeft 변환 (유역,행정 구분별 검색단 세팅) [추후 md파일작성예정] 2025-04-17 09:08:41 +09:00
chanii8 7d4cdb095d feat: 모듈API 권한관리 다이얼로그 작업 2025-04-16 17:53:40 +09:00
JIeunhuh f9cd3c579d 사업이력 - 노후하수관로 / 도시침수 예방사업 페이지 구현(이벤트 처리 및 로직필요) 2025-04-16 17:35:59 +09:00
chanii8 6148ac2df9 feat: 모듈 API 수정 페이지 작업 2025-04-16 15:53:48 +09:00
chanii8 6fd1154337 feat: 모듈 API 등록 페이지 작업 2025-04-16 15:44:59 +09:00
chanii8 49ce84254f feat: 모듈 API 관리 페이지 작업 2025-04-16 15:18:15 +09:00
leedj312 42b6dfc616 테이블 헤더 고정 및 선택 시 text bold 처리 2025-04-16 11:07:54 +09:00
chanii8 f264a08497 refactor: 컨펌 로직 정리 2025-04-16 10:28:53 +09:00
JIeunhuh 969a0eab29 Merge commit 'a5c3405049094e015c7e159e4b1c65a4b851396b' into iipbs/sn/oi 2025-04-16 09:48:28 +09:00
chanii8 9cc14b1186 chore: 빌드 에러 수정 2025-04-15 17:16:02 +09:00
chanii8 d2386d1efd feat: 콘텐츠 관리 목록 페이지 작업 2025-04-15 17:09:08 +09:00
chanii8 155557f6ae feat: 공통 기능 rewrites 로직 작성 2025-04-15 15:29:55 +09:00
chanii8 06289f3e65 chore: cs rewrites 로직 제거 2025-04-14 16:30:03 +09:00
YangJinSoo94 a5c3405049 재조회했을 경우 resetPageNo prop에 따라 1페이지로 돌아가도록 수정 2025-04-14 14:28:53 +09:00
chanii8 604c540a68 chore: env 변수 추가 2025-04-14 13:27:56 +09:00
chanii8 8d05a31f18 feat: 게시판 관리 프로젝트 생성 2025-04-14 11:22:48 +09:00
chanii8 5c9ad9b7d9 feat: 게시판 관리 페이지 rewrites 로직 적용 2025-04-14 11:22:10 +09:00
YangJinSoo94 0267ffb0d9 UiTable에서 header속성에 align right 이 있을 우측정렬 하도록 수정 2025-04-11 17:38:41 +09:00
chanii8 38119c6760 Merge pull request 'iipbs_cs' (#15) from iipbs_cs into main
Reviewed-on: #15
2025-04-11 15:57:07 +09:00
chanii8 311ffdc798 Merge remote-tracking branch 'origin/main' into iipbs_cs 2025-04-11 15:56:43 +09:00
chanii8 b9a5938761 feat: 약관관리 페이지 작업 2025-04-11 15:15:12 +09:00
chanii8 0ef244a5b2 design: 페이지 레이아웃 하단 여백 수정 2025-04-11 14:31:58 +09:00
chanii8 73393426aa feat: tinymce 에디터 작업 2025-04-11 11:44:14 +09:00
chanii8 694cd4893e chore: getCurrentMenu 수정 2025-04-10 18:18:35 +09:00
chanii8 93e066ebe5 feat: 템플릿 관리 페이지 작업 2025-04-10 18:05:57 +09:00
YangJinSoo94 21a25de096 UiModal noheaderYn, noFooterYn 을 넣으면 헤더와 풋터영역 안보이도록 수정 2025-04-10 16:18:29 +09:00
chanii8 1f99d450e9 refactor: tus 업로드 커스텀 훅 생성 2025-04-10 14:39:19 +09:00
chanii8 e456595868 feat: 메뉴 권한 관리 다이얼로그 작업 2025-04-10 10:13:03 +09:00
leedj312 e4dda6e50b 테이블 클릭 초기값 -1로 변경 2025-04-09 17:25:46 +09:00
chanii8 83ba372a17 rename: WithModePost -> PostPayloadData 2025-04-09 16:59:45 +09:00
chanii8 d5bc4c30f2 rename: requestPosting -> savePosting 2025-04-09 16:32:03 +09:00
chanii8 33219d03bc rename: serial -> sequential 2025-04-09 16:03:10 +09:00
chanii8 52343926d3 feat: 템플릿 관리 폼 작업 2025-04-09 15:52:29 +09:00
chanii8 ec4b7db2f3 feat: 템플릿 관리 페이지 생성 2025-04-09 13:06:44 +09:00
leedj312 5898d454aa 모달 트리거 변경으로 수정 및 테이블 셀클릭 이벤트 파라미터추가(셀 head값) 2025-04-09 10:14:07 +09:00
chanii8 dc1abe83fa refactor: next image 'objectFit' 속성 제거 2025-04-09 09:31:12 +09:00
chanii8 12a12339b9 feat: 배너 이미지 미리보기 다이얼로그 작업 2025-04-08 18:10:27 +09:00
chanii8 661eb3597a feat: 배너 목록 이미지에 스켈레톤 적용 2025-04-08 17:00:59 +09:00
chanii8 2eb9eb9370 fix: atflCd 쿠키 초기화 로직 추가 2025-04-08 16:28:27 +09:00
chanii8 07ee7adca5 refactor: atflCd로 기존 이미지 삭제하는 로직으로 수정 2025-04-08 15:57:19 +09:00
chanii8 a5652f9f04 feat: 이미지 업로드 로직에 기존 이미지 삭제 로직 추가 2025-04-08 14:33:11 +09:00
yschoi 72f08c0f38 api 주소 변경경 2025-04-08 13:59:16 +09:00
chanii8 77539bb785 refactor: form wrapper 선언 및 적용 2025-04-08 13:59:04 +09:00
chanii8 d6b48ea554 refactor: blob 이미지 가져오는 방식 수정 2025-04-08 10:39:04 +09:00
leedj312 690907364e Merge commit '0a5ea769716d3c85f917de8deca45cc3dc472f35' into iipbs/common/main 2025-04-07 17:53:38 +09:00
leedj312 dc4c1e93b1 datePicker value 수정 2025-04-07 17:53:12 +09:00
chanii8 2797ec210e fix: getCurrentMenu 함수 수정 2025-04-07 17:38:37 +09:00
truthwing 0a5ea76971 css 적용되도록 수정.
table에 클릭시 opacity 적용.
2025-04-07 17:18:10 +09:00
chanii8 9a2bdfc0ca feat: 배너 관리 페이지 작업 2025-04-07 17:05:55 +09:00
chanii8 a1dd0dc931 feat: textarea 컴포넌트 추가 2025-04-07 16:43:55 +09:00
YangJinSoo94 18d7698c03 표 className 추가 (호출시 속성없어도 오류 x) 2025-04-07 15:32:20 +09:00
YangJinSoo94 02a3a47ad0 원복2 2025-04-07 15:12:58 +09:00
YangJinSoo94 55370f21d2 원복... 2025-04-07 15:08:47 +09:00
YangJinSoo94 c1540030a5 UiTable className 파라미터 추가 2025-04-07 15:06:25 +09:00
chanii8 7e55711506 chore: Image 인터페이스 수정(imgTypeCd) 2025-04-07 14:00:29 +09:00
chanii8 3a8039c5d2 chore: endpoint key 수정 2025-04-07 13:53:17 +09:00
chanii8 0e7c59f084 feat: 사이트 관리 및 이미지 관리 페이지 테이블에 페이지네이션 적용 2025-04-07 13:21:19 +09:00
chanii8 35d9595ea2 feat: data table 페이지네이션 ui 추가 2025-04-07 13:11:22 +09:00
chanii8 632042b19c feat: 이미지 뷰잉 작업(이미지 관리 상세) 2025-04-07 10:25:56 +09:00
YangJinSoo94 1ac35dfc33 listdiv 크기 조정을 위하여 className을 받음 2025-04-07 09:33:50 +09:00
chanii8 bc6ee47734 feat: 이미지 뷰잉 작업 2025-04-04 18:40:30 +09:00
chanii8 b0f8638df2 feat: 사이트 반영 api 날짜 payload 수정 2025-04-04 16:24:57 +09:00
YangJinSoo94 e62ddf2877 listdiv overflow class 추가 2025-04-04 16:14:18 +09:00
chanii8 7f4e973c5f feat: 사이트 반영 저장 api 작업 2025-04-04 15:25:34 +09:00
chanii8 528cc39831 feat: 사이트 반영 팝업 작업 2025-04-04 14:49:00 +09:00
chanii8 d84bfe2a7c fix: 스토리북 "Failed to fetch dynamically imported module" 에러 해결 2025-04-04 11:05:57 +09:00
chanii8 badb682163 refactor: 사이트 반영 폼 리팩토링 2025-04-03 18:39:54 +09:00
leedj312 7c1cc6df65 datepicker 한글 및 선택후 팝오버 추가 2025-04-03 18:26:39 +09:00
leedj312 23d8425858 Merge commit '6e390199b3518f0b88641c01b0b5dc91ac8e32bc' into iipbs/common/main 2025-04-03 18:25:30 +09:00
YangJinSoo94 6e390199b3 max 삭제 2025-04-03 18:22:36 +09:00
YangJinSoo94 6e10389b33 다시 commit 2025-04-03 17:52:10 +09:00
chanii8 e659f47d1d feat: 사이트 반영 폼 작업 2025-04-03 17:37:35 +09:00
chanii8 e3454e11d5 feat: 사이트 게시 이력 api 작업 2025-04-03 11:40:00 +09:00
chanii8 6df605a184 feat: loading ui 적용 2025-04-03 09:37:25 +09:00
leedj312 4b864019df Merge commit '8fd441bcbfadf5c0b54248e4e320863401960536' into iipbs/common/main 2025-04-02 18:21:45 +09:00
leedj312 f66bbed334 date picker ko / 테이블 classname hight 부분 삭제 2025-04-02 18:18:28 +09:00
chanii8 483e0b319a chore: 이미지 업로드 안내 문구 추가 2025-04-02 17:47:14 +09:00
truthwing 8fd441bcbf 테이블 크기 전체로 잡게 수정 완료. 2025-04-02 17:45:38 +09:00
YangJinSoo94 da723e74b9 Merge branch 'iipbs/common/main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/common/main 2025-04-01 14:20:15 +09:00
YangJinSoo94 2323c67131 페이지네이션 수정 완료 2025-04-01 14:20:05 +09:00
truthwing 05b3b0675f 테이블에 object 타입도 받을 수 있도록 수정. 2025-04-01 13:12:17 +09:00
YangJinSoo94 f88e582b5e 테이블 잘못 commit원복, 페이지네이션 재수정 2025-04-01 11:37:00 +09:00
YangJinSoo94 5afc8da9a8 페이지네이션 리스트가 많아도 10개 페이지넘버로 다음,이전으로 넘어가도록 수정 2025-04-01 11:23:46 +09:00
leedj312 27499977d7 Merge remote-tracking branch 'origin/iipbs/common/main' into iipbs/common/main 2025-03-28 18:02:37 +09:00
leedj312 554ad2c5a8 searInput 오류 일단 수정 및 테이블 헤더 ? 추가 2025-03-28 18:02:13 +09:00
YangJinSoo94 fa9f971226 dialog 상단 제목 없으면 제목없이 모달 출력되도록 수정 2025-03-28 11:34:08 +09:00
yschoi f7dee210e9 Merge remote-tracking branch 'origin/iipbs/common/main' into iipbs/sn/dev 2025-03-27 17:01:33 +09:00
yschoi 23da019a27 chore: .env.local 파일의 API URL을 개발 서버로 변경 및 Pplines 컴포넌트의 검색 폼 구조 개선 2025-03-27 16:56:52 +09:00
YangJinSoo94 2919d7d637 alert dialog 수정 2025-03-27 16:36:41 +09:00
YangJinSoo94 ec89b1fbf3 Merge branch 'iipbs/common/main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/common/main 2025-03-27 16:34:52 +09:00
YangJinSoo94 a764bde899 modal 수정정 2025-03-27 16:34:41 +09:00
yschoi c1531e3451 chore: .env 파일 무시 설정 변경 2025-03-27 16:20:04 +09:00
leedj312 4667e535e9 select val type변경 2025-03-27 16:10:20 +09:00
yschoi 10184f07e0 Merge remote-tracking branch 'origin/main' into iipbs/sn/dev 2025-03-27 15:52:28 +09:00
leedj312 4856e7fc17 index오류 수정 2025-03-27 15:38:03 +09:00
leedj312 a263ec0961 selectbox1 val 타입 변경 alert_dialog_ui 및 dialog_ui로 변경 2025-03-27 15:24:09 +09:00
yschoi 2955ff9702 feat: 새로운 구성 요소 및 환경 설정 추가, 기존 코드 개선 2025-03-27 14:32:47 +09:00
YangJinSoo94 33d82eff22 표 스타일 완성 2025-03-27 13:21:42 +09:00
YangJinSoo94 fa209d0c5d 스타일 적용 중 (표영역) 2025-03-27 11:04:59 +09:00
YangJinSoo94 9c0ca52036 title영역, main_contents 영역 수정 2025-03-27 10:07:09 +09:00
leedj312 1624b44c0d uimodal 추가 2025-03-27 09:22:03 +09:00
YangJinSoo94 e781af3209 selectbox 원복 2025-03-26 17:48:52 +09:00
YangJinSoo94 8346f59f45 selectbox, search_input 스타일 수정 2025-03-26 17:32:28 +09:00
YangJinSoo94 20442392ac tab export 누락 수정 2025-03-26 17:20:28 +09:00
YangJinSoo94 4b13eb4280 div영역 공통화 (Main, SearchDiv, ContentDiv, Listdiv..) 2025-03-26 17:01:19 +09:00
leedj312 c966581a99 selectBox default value 추가 2025-03-26 16:56:55 +09:00
YangJinSoo94 a147d972b8 base.css 복구 및 css작업 진행중 2025-03-26 14:52:40 +09:00
YangJinSoo94 ea7cb87c7b Merge remote-tracking branch 'origin/main' into iipbs/common/main 2025-03-26 14:41:05 +09:00
YangJinSoo94 8f1bb4b5f2 -TableDiv > ContentsDiv
-css수정 작업 진행중
2025-03-26 13:34:52 +09:00
YangJinSoo94 4e7f40d848 Merge remote-tracking branch 'origin/main' into iipbs/common/main 2025-03-26 09:59:06 +09:00
yschoi a1171a6d02 Merge remote-tracking branch 'origin/main' into iipbs/sn/dev 2025-03-24 13:23:47 +09:00
yschoi 409b75c8ff 타이틀 수정 2025-03-24 11:20:26 +09:00
YangJinSoo94 54803e4061 탭 공통영역으로 개발 (tabList, defaultValue 파라미터) 2025-03-24 11:06:39 +09:00
YangJinSoo94 076516fdbf 오타 수정 2025-03-24 09:40:43 +09:00
YangJinSoo94 ac5573a296 cellClick 에러수정 2025-03-21 17:43:02 +09:00
YangJinSoo94 6fccf422ea search_form_left, right / table_div 추가가 2025-03-21 16:15:25 +09:00
YangJinSoo94 0a29570cda 메인영역 및 검색영역 스타일 지정 중 2025-03-21 15:49:43 +09:00
YangJinSoo94 cc727045e1 select, search_iinput_select 초기화가 될수 있도록 파라미터 수정 2025-03-21 11:36:05 +09:00
YangJinSoo94 f0f2665981 searchInput value및 setText 이벤트를 받도록 수정 2025-03-20 17:43:16 +09:00
YangJinSoo94 9ea9dc5e32 테이블 헤더 글씨체 진하게 및 margin 적용 2025-03-19 09:34:30 +09:00
yschoi 97b736bf0d 관로시설 데이터 조회회 2025-03-18 19:43:51 +09:00
YangJinSoo94 23bc71b275 UI테이블 cellclick 타입 수정 2025-03-18 15:48:51 +09:00
YangJinSoo94 c972dffa6e Merge branch 'iipbs/common/main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/common/main 2025-03-18 14:40:27 +09:00
YangJinSoo94 9e84a452ff undefined관련 호출 수정정 2025-03-18 14:40:14 +09:00
truthwing 20a133119c Merge commit 'e7f2b3e3ee97fd61513ab84ca6832361936c5411' into iipbs/common/main 2025-03-18 09:22:13 +09:00
truthwing ccf20ee7eb Merge commit '08e9cb98e9388b1422890841a45775c03e52c7b0' into iipbs/common/main 2025-03-17 18:27:55 +09:00
truthwing c79966d17e tailwind 클래스 적용이 내부적으로 동작되도록 수정.
내부적으로 클래스 적용이 안되었던 부분에서 깨지는 부분 수정.
2025-03-11 15:49:41 +09:00
truthwing 0c53e25c61 Merge branch 'iipbs/common/main' into iipbs/sn/dev 2025-03-11 14:43:13 +09:00
yschoi 97d74250a5 관로시설 조회 2025-03-11 12:04:36 +09:00
truthwing 7bb973c912 기본 구조 작성 완료. 2025-03-06 18:44:37 +09:00
truthwing 043f4b5da9 init 2025-03-06 18:25:20 +09:00
543 changed files with 22327 additions and 5471 deletions
+19
View File
@@ -0,0 +1,19 @@
name: iipbs_sn
on:
push:
branches: [iipbs/sn/dev]
jobs:
build:
runs-on: ubuntu-20.04
steps:
- uses: appleboy/ssh-action@master
with:
port: 11022
host: con.mind-one.co.kr
username: mindone
password: mindone!23
script: |
cd ./iipbs/front
sh ./frontStartSn.sh
+2
View File
@@ -14,6 +14,8 @@ dist
# misc
.DS_Store
apps/wr/.next/**/*
apps/fo/.next/**/*
# env
.env*
+40
View File
@@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.env*
+3
View File
@@ -0,0 +1,3 @@
{
"plugins": ["prettier-plugin-tailwindcss"]
}
@@ -0,0 +1,2 @@
import { BoardPage } from "@/pages/board";
export default BoardPage;
@@ -0,0 +1,35 @@
"use client";
import { usePathname, useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
import type { MenuItem } from "@common/components/api";
import { BaseLayout } from "@common/components/ui";
import { useMenuStore } from "@/shared/lib";
import { PageLayout } from "@/shared/ui/page-layout";
export default function WithBaseLayout({
children,
}: {
children: React.ReactNode;
}) {
const router = useRouter();
const pathname = usePathname();
const queryClient = useQueryClient();
const { setCurrentMenuInfo } = useMenuStore((state) => state);
const handleMenuClick = (item: MenuItem) => {
setCurrentMenuInfo(item);
router.push(item.urlPath);
};
return (
<BaseLayout
queryClient={queryClient}
currentPath={pathname ?? ""}
handleMenuClick={handleMenuClick}
>
<PageLayout>{children}</PageLayout>
</BaseLayout>
);
}
@@ -0,0 +1,2 @@
import { SkeletonPage } from "@/shared/ui/skeleton-page";
export default SkeletonPage;
@@ -0,0 +1,100 @@
import {
ACCESS_TOKEN,
AUTH_USER_ID,
CLAIM_NAME,
REFRESH_TOKEN,
SERVER_API_URL,
SITE_ID,
} from "@/shared/config";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
async function handler(
request: Request,
params: { url: string[] },
method: "GET" | "POST" | "PUT" | "DELETE",
) {
const cookieStore = await cookies();
const headers: Record<string, string> = {
"Content-Type": "application/json",
"X-Site-Id": SITE_ID,
};
let body = {};
const isRefreshHandler = params.url.includes("refresh");
// silent refresh
if (isRefreshHandler) {
const refreshToken = cookieStore.get(REFRESH_TOKEN);
headers[CLAIM_NAME] = refreshToken?.value ?? "";
if (!headers[CLAIM_NAME]) {
return NextResponse.json({ message: "Can't refresh" }, { status: 401 });
}
}
try {
if (!isRefreshHandler) {
body = await request.json();
}
const result = await fetch(`${SERVER_API_URL}/${params.url.join("/")}`, {
method,
headers,
body: JSON.stringify(body),
});
if (!result.ok) {
return NextResponse.json(
{ message: "Invalid Credentials" },
{ status: 401 },
);
}
const accessToken = result.headers.get(ACCESS_TOKEN);
const refreshToken = result.headers.get(REFRESH_TOKEN);
const userId = result.headers.get(AUTH_USER_ID);
if (accessToken && userId) {
const response = NextResponse.json({
[ACCESS_TOKEN]: accessToken,
[AUTH_USER_ID]: userId,
});
if (refreshToken) {
cookieStore.set(REFRESH_TOKEN, refreshToken, {
httpOnly: true,
sameSite: "lax",
path: "/",
});
}
return response;
}
return NextResponse.json(
{ message: "Invalid Credentials" },
{ status: 401 },
);
} catch (error) {
return NextResponse.json(
{ message: "Server Error", error },
{ status: 500 },
);
}
}
export async function POST(
request: Request,
props: { params: Promise<{ url: string[] }> },
) {
const params = await props.params;
return handler(request, params, "POST");
}
export async function PUT(
request: Request,
props: { params: Promise<{ url: string[] }> },
) {
const params = await props.params;
return handler(request, params, "PUT");
}
@@ -0,0 +1,2 @@
import { LoginPage } from "@/pages/auth/login";
export default LoginPage;
+8
View File
@@ -0,0 +1,8 @@
import type { Metadata } from "next";
import { App } from "@/app";
export const metadata: Metadata = {
title: "유역하수도 통합 정보플랫폼",
};
export default App;
+21
View File
@@ -0,0 +1,21 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
rules: {
"react-hooks/exhaustive-deps": "off",
},
},
];
export default eslintConfig;
+37
View File
@@ -0,0 +1,37 @@
import { NextRequest, NextResponse } from "next/server";
import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config";
const serverApiUrl = process.env.NEXT_PUBLIC_SERVER_API_URL;
export function middleware(req: NextRequest) {
const pathname = req.nextUrl.pathname;
const searchParams = req.nextUrl.searchParams;
if (pathname.startsWith("/server")) {
const queryParams = searchParams.toString();
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
queryParams ? `?${queryParams}` : ""
}`;
return NextResponse.rewrite(new URL(newUrl));
}
if (pathname.startsWith("/attach-service")) {
const requestHeaders = new Headers(req.headers);
const token = req.cookies.get(CLAIM_NAME)?.value;
const userId = req.cookies.get(AUTH_USER_ID)?.value;
const newUrl = `${serverApiUrl}${pathname}`;
if (token) requestHeaders.set(CLAIM_NAME, token);
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
return NextResponse.rewrite(new URL(newUrl), {
headers: requestHeaders,
});
}
return NextResponse.next();
}
export const config = {
matcher: ["/server/:path*", "/attach-service/:path*"],
};
+5
View File
@@ -0,0 +1,5 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {};
export default nextConfig;
+50
View File
@@ -0,0 +1,50 @@
{
"name": "feature",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev -p 13000",
"build": "next build",
"start": "next start -p 13000",
"lint": "next lint"
},
"dependencies": {
"@common/assets": "workspace:*",
"@common/components": "workspace:*",
"@frontend-opensource/use-react-hooks": "^1.1.4",
"@hookform/resolvers": "^4.1.3",
"@minoru/react-dnd-treeview": "^3.5.0",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@tanstack/react-query": "^5.67.1",
"@tanstack/react-table": "^8.21.2",
"@tinymce/tinymce-react": "^6.1.0",
"axios": "^1.8.1",
"chart.js": "^4.4.8",
"dayjs": "^1.11.13",
"js-cookie": "^3.0.5",
"next": "15.1.7",
"react-chartjs-2": "^5.3.0",
"react-dnd": "^16.0.1",
"react-hook-form": "^7.54.2",
"tailwindcss-animate": "^1.0.7",
"tus-js-client": "^4.3.1",
"zod": "^3.24.2",
"zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@faker-js/faker": "^9.5.0",
"@types/js-cookie": "^3.0.6",
"@types/node": "^22.13.1",
"@types/webpack": "^5.28.5",
"eslint": "^9",
"eslint-config-next": "15.1.7",
"postcss": "^8",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11",
"steiger": "^0.5.5",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
View File
+8
View File
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;
Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

+18
View File
@@ -0,0 +1,18 @@
import "./styles/base.css";
import "./styles/components.css";
import { AppProvider } from "./providers";
import { cookies } from "next/headers";
const App = async ({ children }: Readonly<{ children: React.ReactNode }>) => {
const theme = (await cookies()).get("site-theme-key");
return (
<html lang="ko" data-theme={theme?.value}>
<body>
<AppProvider>{children}</AppProvider>
</body>
</html>
);
};
export { App };
@@ -0,0 +1,28 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
import { AuthWrapper } from "@/entities/auth";
import { MenuWrapper } from "@/entities/menu";
import { StoreProvider } from "@/shared/providers";
import { AlertDialog } from "@/shared/ui/alert-dialog";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
<StoreProvider>
<ConfirmProvider>
<AuthWrapper>
<MenuWrapper>{children}</MenuWrapper>
<AlertDialog />
<ConfirmDialog />
</AuthWrapper>
</ConfirmProvider>
</StoreProvider>
</QueryClientProvider>
);
};
export { AppProvider };
@@ -0,0 +1 @@
export { AppProvider } from "./app-provider";
@@ -0,0 +1,11 @@
@import "@common/assets/styles/base.css";
@import "@common/assets/styles/diveicon.css";
@tailwind base;
@tailwind utilities;
html,
body {
font-family: "Noto Sans KR", sans-serif;
font-size: 10px;
}
@@ -0,0 +1,129 @@
.card {
position: relative;
margin: 0.6rem;
box-sizing: border-box;
min-height: 2rem;
border-radius: 0.5rem;
padding: 0.6rem;
font-size: 1.3rem;
color: #666;
}
.card_width10 {
width: calc(10% - 12px);
}
.card_width20 {
width: calc(20% - 12px);
}
.card_width25 {
width: calc(25% - 12px);
}
.card_width30 {
width: calc(30% - 12px);
}
.card_width33 {
width: calc(33.333% - 12px);
}
.card_width40 {
width: calc(40% - 12px);
}
.card_width45 {
width: calc(45% - 12px);
}
.card_width50 {
width: calc(50% - 12px);
}
.card_width60 {
width: calc(60% - 12px);
}
.card_width66 {
width: calc(66.666% - 12px);
}
.card_width70 {
width: calc(70% - 12px);
}
.card_width75 {
width: calc(75% - 12px);
}
.card_width80 {
width: calc(80% - 12px);
}
.card_width90 {
width: calc(90% - 12px);
}
.card_width100 {
width: calc(100% - 12px);
}
.card_nopadding {
padding: 0 !important;
}
.card_nomargin {
width: 100%;
margin: 0 !important;
padding: 0 !important;
}
.topline {
border-top: 1px solid #ccc;
}
.bottomline {
border-bottom: 1px solid #e6eef3;
background: #fff;
}
.card-border {
border: 1px solid #dbe2e6;
background-color: rgba(255, 255, 255, 0.6);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.card-header {
margin-top: -0.6rem;
margin-left: -0.6rem;
margin-right: -0.6rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e1e7eb;
padding: 1rem;
}
.card-footer {
margin-left: -0.6rem;
margin-right: -0.6rem;
margin-top: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
border-top: 1px solid #e1e7eb;
padding-top: 1rem;
padding-bottom: 1rem;
}
/* 블랙 테마 */
[data-theme="black"] {
.card-border {
background-color: rgba(255, 255, 255, 0.065);
border-color: #38383a;
}
.card-header {
border-bottom-color: #3f4042;
}
}
@@ -0,0 +1,43 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
Attachment,
AttachmentService,
} from "../model/attachment-interface";
export class AttachmentApiService implements AttachmentService {
public async getAttachments(atflCd: string): Promise<Attachment[]> {
const response = await client.get<Attachment[]>(
`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflCd}`,
);
return response.data;
}
public async deleteAttachment(atflId: string): Promise<void> {
await client.delete(`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflId}`);
}
public async deleteAttachmentByAtflCd(atflCd: string): Promise<void> {
await client.delete(
`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/atflCd/${atflCd}`,
);
}
public async getImageUrl(atflId: string): Promise<string> {
if (!atflId) return "";
const response = await fetch(`${ENDPOINT.ATTACH_SERVICE.IMAGES}/${atflId}`);
const blob = await response.blob();
return URL.createObjectURL(blob);
}
public async getImageUrlByAtflCd(atflCd: string): Promise<string> {
if (!atflCd) return "";
const response = await fetch(
`${ENDPOINT.ATTACH_SERVICE.IMAGES}/atflCd/${atflCd}`,
);
const blob = await response.blob();
return URL.createObjectURL(blob);
}
}
export const AttachmentApi = new AttachmentApiService();
@@ -0,0 +1 @@
export { AttachmentApi } from "./api/attachment-service";
@@ -0,0 +1,22 @@
export interface Attachment {
atflCd: string;
atflSn: number;
atflId: string;
physFileNm: string;
orgnlFileNm: string;
atflFlieSz: number;
atflType: string;
dwnldNocs: number;
delYn: string;
lnkgDmnId: string;
lnkgDmnNm: string;
regDt: string;
}
export interface AttachmentService {
getAttachments: (atflCd: string) => Promise<Attachment[]>;
deleteAttachment: (atflId: string) => Promise<void>;
deleteAttachmentByAtflCd: (atflCd: string) => Promise<void>;
getImageUrl: (atflId: string) => Promise<string>;
getImageUrlByAtflCd: (atflCd: string) => Promise<string>;
}
@@ -0,0 +1,85 @@
import { AxiosError } from "axios";
import {
ENDPOINT,
JWT_EXPIRATION_TIME,
ACCESS_TOKEN,
AUTH_USER_ID,
CLAIM_NAME,
} from "@/shared/config";
import type { LoginService, ILogin } from "../model/login-interface";
import { tokenManager } from "../lib/token-manager";
export class LoginApiService implements LoginService {
private static instance: LoginApiService;
private constructor() {}
public static getInstance(): LoginApiService {
if (!LoginApiService.instance) {
LoginApiService.instance = new LoginApiService();
}
return LoginApiService.instance;
}
public async login(payload: ILogin): Promise<void> {
try {
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
method: "POST",
body: JSON.stringify({
userId: payload.userId,
password: payload.password,
}),
});
const data = await response.json();
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
throw new Error("Missing required authentication tokens");
}
tokenManager.setToken(CLAIM_NAME, accessToken);
tokenManager.setToken(AUTH_USER_ID, tokenId);
tokenManager.setupRefreshTimer(
() => this.silentRefresh(),
JWT_EXPIRATION_TIME,
);
} catch (error) {
if (error instanceof AxiosError) {
throw new Error(`Login failed: ${error.response?.data?.message}`);
}
throw new Error(`Login failed: ${error}`);
}
}
public async silentRefresh(): Promise<void> {
try {
const response = await fetch(
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
{ method: "PUT" },
);
const data = await response.json();
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
throw new Error("Missing required authentication tokens");
}
tokenManager.setToken(CLAIM_NAME, accessToken);
tokenManager.setToken(AUTH_USER_ID, tokenId);
} catch (error) {
tokenManager.clearAuthToken();
if (error instanceof AxiosError) {
throw new Error(
`Silent refresh failed: ${error.response?.data?.message}`,
);
}
throw new Error(`Silent refresh failed: ${error}`);
}
}
}
export const LoginApi = LoginApiService.getInstance();
@@ -0,0 +1,2 @@
export { LoginApi } from "./api/login-service";
export { AuthWrapper } from "./lib/auth-wrapper";
@@ -0,0 +1,49 @@
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { client } from "@/shared/api";
import type { IUserInfo } from "@/shared/store";
import { getCookie, useUserStore } from "@/shared/lib";
import { ENDPOINT, AUTH_USER_ID } from "@/shared/config";
import { LoginApi } from "../api/login-service";
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
const router = useRouter();
const { userInfo, setUserInfo } = useUserStore((state) => state);
async function authCheck() {
const userId = getCookie(AUTH_USER_ID);
if (!userId) {
throw new Error("Missing required authentication tokens");
}
const response = await client.get<IUserInfo>(
`${ENDPOINT.USER_SERVICE.USERS}/${userId}`,
);
if (!userInfo) setUserInfo(response.data);
}
useEffect(() => {
(async () => {
try {
await authCheck();
} catch (error) {
console.error(error);
try {
await LoginApi.silentRefresh();
await authCheck();
} catch (refreshError) {
console.error("Token refresh failed:", refreshError);
setUserInfo(null);
router.push("/auth/login");
}
}
})();
}, []);
return <>{children}</>;
};
export { AuthWrapper };
@@ -0,0 +1,59 @@
import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config";
import { getCookie, setCookie, removeCookie } from "@/shared/lib";
export class TokenManager {
private static instance: TokenManager;
private refreshTimer: NodeJS.Timeout | null = null;
private constructor() {}
public static getInstance(): TokenManager {
if (!TokenManager.instance) {
TokenManager.instance = new TokenManager();
}
return TokenManager.instance;
}
public getToken(key: string): string | undefined {
return getCookie(key);
}
public setToken(key: string, value: string): void {
setCookie(key, value, {
secure: window.location.protocol === "https:",
sameSite: "lax",
});
}
public removeToken(key: string): void {
removeCookie(key);
}
public setupRefreshTimer(
callback: () => Promise<void>,
expirationTime: number,
): void {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
}
this.refreshTimer = setTimeout(() => {
callback().catch((error) => {
console.error("Silent refresh failed:", error);
this.clearAuthToken();
});
}, expirationTime - 60000);
}
public clearAuthToken(): void {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
this.refreshTimer = null;
}
this.removeToken(CLAIM_NAME);
this.removeToken(AUTH_USER_ID);
}
}
export const tokenManager = TokenManager.getInstance();
@@ -0,0 +1,9 @@
export interface ILogin {
userId: string;
password: string;
}
export interface LoginService {
login(payload: ILogin): Promise<void>;
silentRefresh(): Promise<void>;
}
@@ -0,0 +1,21 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
BoardPopupList,
BoardPopupParams,
BoardService,
} from "../model/board-interface";
export class BoardApiService implements BoardService {
public async getBoardPopupList(
params: BoardPopupParams,
): Promise<BoardPopupList> {
const response = await client.get<BoardPopupList>(
`${ENDPOINT.BOARD_SERVICE.BBS}`,
{ params },
);
return response.data;
}
}
export const BoardApi = new BoardApiService();
@@ -0,0 +1,2 @@
export { BoardApi } from "./api/board-service";
export { type BoardPopup } from "./model/board-interface";
@@ -0,0 +1,42 @@
import type { PageableList, PageableParams } from "@/shared/model";
export const BOARD_TYPE = {
BBSNORMAL: "BBSNORMAL",
BBSFAQ: "BBSFAQ",
BBSIMAGE: "BBSIMAGE",
BBSMOVIE: "BBSMOVIE",
} as const;
export type BoardType = (typeof BOARD_TYPE)[keyof typeof BOARD_TYPE];
export interface BoardPopupParams extends PageableParams {
bbsNm?: string;
}
export interface BoardPopup {
bbsId: number;
bbsTypeCd: BoardType;
bbsTypeNm: string;
bbsNm: string;
bbsExpln: string;
rfrncUseYn: string;
atflUseYn: string;
atflMaxCnt: number;
atflMaxFileSz: number;
ansUseYn: string;
cmntUseYn: string;
dgstfnEvlUseYn: string;
smsUseYn: string;
upndFixUseYn: string;
pstgPrdUseYn: string;
popupUseYn: string;
delYn: string;
regDt: string;
pstCnt: number;
}
export type BoardPopupList = PageableList<BoardPopup>;
export interface BoardService {
getBoardPopupList: (params: BoardPopupParams) => Promise<BoardPopupList>;
}
@@ -0,0 +1 @@
export { CodeSelect } from "../ui/code-select";
@@ -0,0 +1,14 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { CodeService, ICode } from "../model/code-interface";
export class CodeApiService implements CodeService {
public async getCode(upCd: string): Promise<ICode[]> {
const response = await client.get<ICode[]>(
`${ENDPOINT.CMS_SERVICE.CODE}/${upCd}`,
);
return response.data;
}
}
export const CodeApi = new CodeApiService();
@@ -0,0 +1 @@
export { CodeSelect } from "./ui/code-select";
@@ -0,0 +1,9 @@
export interface ICode {
lwpoCd: string;
upCd: string;
lwpoCdNm: string;
}
export interface CodeService {
getCode(upCd: string): Promise<ICode[]>;
}
@@ -0,0 +1,42 @@
import { useQuery } from "@tanstack/react-query";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@common/components";
import { ENDPOINT } from "@/shared/config";
import { CodeApi } from "../api/code-service";
type CodeSelectProps = {
upCd: string;
} & React.ComponentPropsWithoutRef<typeof Select>;
function CodeSelect({ upCd, defaultValue, ...props }: CodeSelectProps) {
const { data: code } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.CODE, upCd],
queryFn: () => CodeApi.getCode(upCd),
enabled: !!upCd,
});
const defaultValueText =
code?.find((item) => item.lwpoCd === defaultValue)?.lwpoCdNm ?? "선택";
return (
<Select {...props}>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder={defaultValueText} />
</SelectTrigger>
<SelectContent>
{code?.map((item) => (
<SelectItem key={item.lwpoCd} value={item.lwpoCd}>
{item.lwpoCdNm}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
export { CodeSelect };
@@ -0,0 +1,19 @@
import { client } from "@/shared/api";
import type {
EditorService,
FileInfo,
UploadFile,
} from "../model/editor-interface";
import { ENDPOINT } from "@/shared/config";
export class EditorApiService implements EditorService {
async uploadImage(data: UploadFile): Promise<FileInfo> {
const response = await client.post<FileInfo>(
ENDPOINT.ATTACH_SERVICE.EDITOR_UPLOAD,
data,
);
return response.data;
}
}
export const EditorApi = new EditorApiService();
@@ -0,0 +1 @@
export { Editor } from "./ui/editor";
@@ -0,0 +1,10 @@
export const blobToBase64 = (blob: Blob): Promise<string> => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = () => {
const base64 = reader.result as string;
resolve(base64.split(",")[1] || "");
};
reader.readAsDataURL(blob);
});
};
@@ -0,0 +1,16 @@
import { blobToBase64 } from "./blob-to-base64";
export const generateFileInfo = async (blobInfo: {
blob: () => Blob;
filename: () => string;
}) => {
const blob = blobInfo.blob();
const base64 = await blobToBase64(blob);
return {
fieldName: `${blob.type.split("/")[1]}`,
fileType: blob.type,
fileBase64: base64,
originalName: blobInfo.filename(),
size: blob.size,
};
};
@@ -0,0 +1,37 @@
import { SERVER_API_URL } from "@/shared/config";
export const editorBaseConfig = {
apiKey: process.env.NEXT_PUBLIC_TINY_API_KEY,
init: {
language: "ko_KR",
height: 500,
plugins: [
"advlist",
"autolink",
"lists",
"link",
"image",
"charmap",
"preview",
"anchor",
"searchreplace",
"visualblocks",
"code",
"codesample",
"fullscreen",
"insertdatetime",
"media",
"table",
"code",
"help",
"wordcount",
"save",
],
toolbar: [
"undo redo | blocks fontfamily fontsize | bold italic underline strikethrough forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist blockquote | table",
"link image",
],
images_upload_base_path: SERVER_API_URL,
file_picker_types: "image",
},
};
@@ -0,0 +1,20 @@
export interface UploadFile {
fieldName: string;
fileType: string;
fileBase64: string;
originalName: string;
size: number;
}
export interface FileInfo {
originalFileName: string;
message: string;
fileType: string;
size: number;
uploaded: number;
url: string;
}
export interface EditorService {
uploadImage(data: UploadFile): Promise<FileInfo>;
}
@@ -0,0 +1,60 @@
import { useRef } from "react";
import {
Editor as TinyEditor,
type Editor as TinyEditorInstance,
type IAllProps,
} from "@tinymce/tinymce-react";
import { editorBaseConfig as baseConfig } from "../model/editor-base-config";
import { generateFileInfo } from "../lib/generate-file-info";
import { EditorApi } from "../api/editor-service";
const Editor = ({ ...props }: IAllProps) => {
const editorRef = useRef<TinyEditorInstance | null>(null);
const imagesUploadHandler = (blobInfo: {
blob: () => Blob;
filename: () => string;
}) => {
return new Promise(async (resolve) => {
const fileInfo = await generateFileInfo(blobInfo);
const data = await EditorApi.uploadImage(fileInfo);
resolve(data.url);
});
};
const filePickerCallback = (
callback: (base64: string, meta: { title: string }) => void,
) => {
const input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("accept", "image/*");
input.onchange = async () => {
const file = input.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async () => {
const base64 = reader.result as string;
callback(base64, { title: file.name });
};
reader.readAsDataURL(file);
};
input.click();
};
return (
<TinyEditor
{...baseConfig}
onInit={(_: unknown, editor: TinyEditorInstance) => {
editorRef.current = editor;
}}
init={{
...baseConfig.init,
images_upload_handler: imagesUploadHandler,
file_picker_callback: filePickerCallback,
}}
{...props}
/>
);
};
export { Editor };
@@ -0,0 +1,32 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
MenuAuthorityList,
MenuAuthorityParams,
MenuAuthorityPayloadData,
MenuAuthorityService,
} from "../model/menu-authority-interface";
export class MenuAuthorityApiService implements MenuAuthorityService {
public async getMenuAuthorityList(
params: MenuAuthorityParams,
): Promise<MenuAuthorityList> {
const response = await client.get<MenuAuthorityList>(
`${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`,
{ params },
);
return response.data;
}
public async saveMenuAuthority(
data: MenuAuthorityPayloadData[],
): Promise<void> {
const response = await client.post<void>(
`${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`,
data,
);
return response.data;
}
}
export const MenuAuthorityApi = new MenuAuthorityApiService();
@@ -0,0 +1 @@
export { ManagementAuthorityDialog } from "./ui/management-authority-dialog";
@@ -0,0 +1,41 @@
import { z } from "zod";
import type { PageableList, PageableParams } from "@/shared/model";
export interface MenuAuthorityParams extends PageableParams {
groupId?: string;
menuCd?: number;
useYn?: string;
}
export const menuAuthoritySchema = z.object({
groupId: z.string(),
groupNm: z.string(),
menuCd: z.number(),
mngAuthrtYn: z.string(),
inqAuthrtYn: z.string(),
regAuthrtYn: z.string(),
mdfcnAuthrtYn: z.string(),
delAuthrtYn: z.string(),
useYn: z.string(),
mode: z.string(),
});
export const menuAuthorityPayloadSchema = menuAuthoritySchema.omit({
groupNm: true,
});
export const menuAuthorityFormSchema = z.object({
menuAuthority: z.array(menuAuthorityPayloadSchema),
});
export type MenuAuthority = z.infer<typeof menuAuthoritySchema>;
export type MenuAuthorityList = PageableList<MenuAuthority>;
export type MenuAuthorityPayloadData = z.infer<
typeof menuAuthorityPayloadSchema
>;
export type MenuAuthorityFormData = z.infer<typeof menuAuthorityFormSchema>;
export interface MenuAuthorityService {
getMenuAuthorityList(params: MenuAuthorityParams): Promise<MenuAuthorityList>;
saveMenuAuthority(data: MenuAuthorityPayloadData[]): Promise<void>;
}
@@ -0,0 +1,117 @@
import type { ColumnDef } from "@tanstack/react-table";
import type { UseFormReturn } from "react-hook-form";
import { Checkbox } from "@common/components/ui";
import type {
MenuAuthority,
MenuAuthorityFormData,
MenuAuthorityPayloadData,
} from "./menu-authority-interface";
export const createColumns = (
form: UseFormReturn<MenuAuthorityFormData>,
): ColumnDef<MenuAuthority>[] => {
const getCurrentRow = (i: number) => form.watch("menuAuthority")[i];
const handleCheckedChange =
(i: number, key: keyof MenuAuthorityPayloadData) => (checked: boolean) => {
if (getCurrentRow(i)?.mode === "R") {
form.setValue(`menuAuthority.${i}.mode`, "U");
}
form.setValue(`menuAuthority.${i}.${key}`, checked ? "Y" : "N");
};
return [
{
accessorKey: "groupNm",
header: "그룹명",
},
{
accessorKey: "mngAuthrtYn",
header: "관리권한",
cell: ({ row }) => {
return (
<Checkbox
checked={getCurrentRow(row.index)?.mngAuthrtYn === "Y"}
onCheckedChange={(checked) => {
if (checked) {
handleCheckedChange(
row.index,
"regAuthrtYn",
)(checked as boolean);
handleCheckedChange(
row.index,
"mdfcnAuthrtYn",
)(checked as boolean);
handleCheckedChange(
row.index,
"delAuthrtYn",
)(checked as boolean);
handleCheckedChange(
row.index,
"inqAuthrtYn",
)(checked as boolean);
}
handleCheckedChange(row.index, "mngAuthrtYn")(checked as boolean);
}}
/>
);
},
},
{
accessorKey: "regAuthrtYn",
header: "등록권한",
cell: ({ row }) => {
return (
<Checkbox
checked={getCurrentRow(row.index)?.regAuthrtYn === "Y"}
onCheckedChange={(checked) => {
if (checked) {
handleCheckedChange(
row.index,
"inqAuthrtYn",
)(checked as boolean);
}
handleCheckedChange(row.index, "regAuthrtYn")(checked as boolean);
}}
/>
);
},
},
{
accessorKey: "mdfcnAuthrtYn",
header: "수정권한 ",
cell: ({ row }) => {
return (
<Checkbox
checked={getCurrentRow(row.index)?.mdfcnAuthrtYn === "Y"}
onCheckedChange={handleCheckedChange(row.index, "mdfcnAuthrtYn")}
/>
);
},
},
{
accessorKey: "delAuthrtYn",
header: "삭제권한",
cell: ({ row }) => {
return (
<Checkbox
checked={getCurrentRow(row.index)?.delAuthrtYn === "Y"}
onCheckedChange={handleCheckedChange(row.index, "delAuthrtYn")}
/>
);
},
},
{
accessorKey: "inqAuthrtYn",
header: "조회권한",
cell: ({ row }) => {
return (
<Checkbox
checked={getCurrentRow(row.index)?.inqAuthrtYn === "Y"}
onCheckedChange={handleCheckedChange(row.index, "inqAuthrtYn")}
/>
);
},
},
];
};
@@ -0,0 +1,101 @@
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery } from "@tanstack/react-query";
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Button,
DataTable,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@common/components/ui";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore } from "@/shared/lib/use-alert-store";
import {
menuAuthorityFormSchema,
type MenuAuthorityFormData,
type MenuAuthorityPayloadData,
} from "../model/menu-authority-interface";
import { createColumns } from "../model/menu-authority-table-columns";
import { MenuAuthorityApi } from "../api/menu-authority-service";
type ManagementAuthorityDialogProps = {
menuCd?: number;
} & React.ComponentProps<typeof Dialog>;
function ManagementAuthorityDialog({
menuCd,
...props
}: ManagementAuthorityDialogProps) {
const { confirm } = useConfirm();
const { setMessage: alert } = useAlertStore((state) => state);
const { data: menuAuthorityList } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENU_AUTHRTS, menuCd],
queryFn: () => MenuAuthorityApi.getMenuAuthorityList({ menuCd }),
enabled: !!menuCd,
});
const { mutateAsync: saveMenuAuthority } = useMutation({
mutationFn: (data: MenuAuthorityPayloadData[]) =>
MenuAuthorityApi.saveMenuAuthority(data),
onSuccess: () => {
alert("저장되었습니다.");
},
});
const form = useForm<MenuAuthorityFormData>({
resolver: zodResolver(menuAuthorityFormSchema),
values: {
menuAuthority: menuAuthorityList?.content ?? [],
},
});
const table = useReactTable({
data: menuAuthorityList?.content ?? [],
columns: createColumns(form),
getCoreRowModel: getCoreRowModel(),
});
const handleSave = async () => {
if (await confirm("저장하시겠습니까?")) {
const data = form.getValues("menuAuthority");
const filteredData = data.filter((item) => item.mode !== "R");
await saveMenuAuthority(filteredData);
}
};
return (
<Dialog {...props}>
<DialogTrigger asChild>
<Button className="gap-3" disabled={!menuCd}>
<i className="diveicon di-personal-information" />
권한관리
</Button>
</DialogTrigger>
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>그룹별 메뉴 권한</DialogTitle>
</DialogHeader>
<DataTable table={table} />
<DialogFooter className="!justify-center">
<Button size="lg" onClick={handleSave}>
저장
</Button>
<DialogClose asChild>
<Button size="lg" color="white">
취소
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export { ManagementAuthorityDialog };
@@ -0,0 +1,14 @@
import { client } from "@common/components/api";
import { ENDPOINT } from "@/shared/config";
import type { MenuItem, MenuService } from "../model/menu-interface";
export class MenuApiService implements MenuService {
async getMenuRoles(siteId: string): Promise<MenuItem[]> {
const response = await client.get<MenuItem[]>(
`${ENDPOINT.CMS_SERVICE.MENU_ROLES}/${siteId}`,
);
return response.data;
}
}
export const MenuApi = new MenuApiService();
@@ -0,0 +1 @@
export { MenuWrapper } from "./lib/menu-wrapper";
@@ -0,0 +1,29 @@
"use client";
import { useEffect } from "react";
import { usePathname } from "next/navigation";
import { SITE_ID } from "@/shared/config";
import { useMenuStore } from "@/shared/lib";
import { MenuApi } from "../api/menu-service";
import { getCurrentMenu } from "./utils";
const MenuWrapper = ({ children }: { children: React.ReactNode }) => {
const pathname = usePathname();
const { setCurrentMenuInfo } = useMenuStore((state) => state);
useEffect(() => {
(async () => {
const menuRoles = await MenuApi.getMenuRoles(SITE_ID);
if (!menuRoles) return;
const currentMenu = getCurrentMenu(menuRoles, pathname ?? "");
if (!currentMenu) return;
setCurrentMenuInfo(currentMenu);
})();
}, []);
return <>{children}</>;
};
export { MenuWrapper };
@@ -0,0 +1,20 @@
import type { MenuItem } from "../model/menu-interface";
export const getCurrentMenu = (
menus: MenuItem[],
pathname: string,
): MenuItem | null => {
for (const menu of menus) {
if (
menu.urlPath !== "" &&
(menu.urlPath === pathname || pathname.startsWith(menu.urlPath))
)
return menu;
if (menu.children) {
const children = getCurrentMenu(menu.children, pathname);
if (children) return children;
}
}
return null;
};
@@ -0,0 +1,23 @@
export interface MenuItem {
menuCd: number;
menuKornNm: string;
menuEngNm: string;
parentId: number | null;
sortSeq: number;
iconNm?: string;
children: MenuItem[] | null;
levelNo: number;
urlPath: string;
menuLnkgTypeCd: string;
menuLnkgSn: number;
menuPath: string;
qrcd: string;
useYn: string;
otptYn: string;
newWndwYn: string;
requiredUrlPath: boolean;
}
export interface MenuService {
getMenuRoles(siteId: string): Promise<MenuItem[]>;
}
@@ -0,0 +1,31 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageableList } from "@/shared/model";
import type {
PostService,
PostingParams,
Post,
PostPayloadData,
} from "../model/posting-interface";
export class PostingApiService implements PostService {
public async getPostingHistory(
params: PostingParams,
): Promise<PageableList<Post>> {
const response = await client.get<PageableList<Post>>(
`${ENDPOINT.CMS_SERVICE.POSTINGS}`,
{ params },
);
return response.data;
}
public async savePosting(data: PostPayloadData[]): Promise<Post[]> {
const response = await client.post<Post[]>(
`${ENDPOINT.CMS_SERVICE.POSTINGS}`,
data,
);
return response.data;
}
}
export const PostingApi = new PostingApiService();
@@ -0,0 +1,7 @@
export { PostingApi } from "./api/posting-service";
export {
POSTING_TYPE,
type Post,
type PostPayloadData,
} from "./model/posting-interface";
export { PostingDialog } from "./ui/posting-dialog";
@@ -0,0 +1,50 @@
import type { PageableList, PageableParams } from "@/shared/model";
import { z } from "zod";
export const POSTING_TYPE = {
POPUP: "POPUP",
BANNER: "BANNER",
IMAGE: "IMAGE",
} as const;
export type PostingType = (typeof POSTING_TYPE)[keyof typeof POSTING_TYPE];
export interface PostingParams extends PageableParams {
pstgTrgtNo: number;
}
export const postSchema = z.object({
siteId: z.string(),
siteNm: z.string(),
pstgTypeCd: z.string(),
pstgTypeNm: z.string(),
pstgTrgtNo: z.number(),
pstgBgngDt: z.string().optional(),
pstgEndDt: z.string().optional(),
pstgPrdSeCd: z.string(),
pstgPrdSeNm: z.string(),
delYn: z.string(),
});
export const postPayloadSchema = postSchema
.extend({
mode: z.string(),
})
.omit({
siteNm: true,
pstgTypeNm: true,
pstgPrdSeNm: true,
});
export const postingFormSchema = z.object({
pstg: z.array(postPayloadSchema),
});
export type Post = z.infer<typeof postSchema>;
export type PostPayloadData = z.infer<typeof postPayloadSchema>;
export type PostingFormData = z.infer<typeof postingFormSchema>;
export interface PostService {
getPostingHistory: (params: PostingParams) => Promise<PageableList<Post>>;
savePosting: (data: PostPayloadData[]) => Promise<Post[]>;
}
@@ -0,0 +1,323 @@
import dayjs from "dayjs";
import { useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useFieldArray, useForm } from "react-hook-form";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Dialog,
DialogTrigger,
Button,
DialogContent,
DialogHeader,
DialogTitle,
Badge,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
Checkbox,
Select,
DateRangePicker,
DialogClose,
DialogFooter,
} from "@common/components/ui";
import { cn } from "@common/components/lib";
import { CodeSelect } from "@/entities/code/@x/posting";
import { SiteApi, type Site } from "@/entities/site/@x/posting";
import type { PageableList } from "@/shared/model";
import { ENDPOINT } from "@/shared/config";
import { PostingApi } from "../api/posting-service";
import {
postingFormSchema,
type PostingFormData,
type PostingType,
type PostPayloadData,
} from "../model/posting-interface";
import { useAlertStore } from "@/shared/lib/use-alert-store";
type DateRange = {
from: string;
to?: string;
};
type PostingTarget = {
pstgTrgtNm: string;
pstgTrgtNo: number;
};
type PostingDialogProps = {
postingType: PostingType;
postingTargets: PostingTarget[];
};
const POSTING_TYPE_LABEL = {
POPUP: "팝업",
BANNER: "배너",
IMAGE: "이미지",
} as const;
function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
const { confirm } = useConfirm();
const { setMessage: alert } = useAlertStore((state) => state);
const [selectedRows, setSelectedRows] = useState<number[]>([]);
const [rememberPeriodCode, setRememberPeriodCode] = useState<
Record<string, string>
>({});
const [rememberPeriodDate, setRememberPeriodDate] = useState<
Record<string, DateRange | undefined>
>({});
const { data: siteList } = useQuery<PageableList<Site>>({
queryKey: [ENDPOINT.CMS_SERVICE.SITES],
queryFn: () => SiteApi.getSiteList({}),
});
const { mutateAsync: savePosting } = useMutation({
mutationFn: (data: PostPayloadData[]) => PostingApi.savePosting(data),
onSuccess: () => {
alert("저장되었습니다.");
},
});
const form = useForm<PostingFormData>({
resolver: zodResolver(postingFormSchema),
});
const { fields, append, replace } = useFieldArray({
control: form.control,
name: "pstg",
});
const isFormValid =
fields.length && fields.every((field) => field.pstgPrdSeCd !== "");
const isAllSelected =
siteList && selectedRows.length === siteList.content.length;
const postingPeriod = (siteId: string) => {
const periodCode = Number(rememberPeriodCode[siteId]);
if (periodCode === 99) return "상시";
return `${dayjs().format("YYYY-MM-DD")} ~ ${dayjs()
.add(periodCode, "day")
.format("YYYY-MM-DD")}`;
};
const handleOpenChange = (open: boolean) => {
if (open) return true;
replace([]);
setSelectedRows([]);
setRememberPeriodCode({});
setRememberPeriodDate({});
};
const handleAppendFields = (i: number) => {
const siteId = siteList?.content[i]?.siteId ?? "";
const newFields = postingTargets.map((target) => ({
siteId,
pstgTypeCd: postingType,
pstgTrgtNo: target.pstgTrgtNo,
pstgBgngDt: rememberPeriodDate[siteId]?.from ?? "",
pstgEndDt: rememberPeriodDate[siteId]?.to ?? "",
pstgPrdSeCd: rememberPeriodCode[siteId] ?? "",
delYn: "N",
mode: "C",
}));
append(newFields);
};
const handleUpdateFields =
(siteId: string) => (obj: Record<string, unknown>) => {
const updatedFields = fields.map((field) =>
"siteId" in field && field.siteId === siteId
? { ...field, ...obj }
: field,
);
replace(updatedFields);
};
const handleRemoveFields = (siteId: string) => {
const filteredFields = fields.filter(
(field) => "siteId" in field && field.siteId !== siteId,
);
replace(filteredFields);
};
const handleSelectRow = (i: number) => {
const isSelected = selectedRows.includes(i);
setSelectedRows((prev) =>
isSelected ? prev.filter((j) => j !== i) : [...prev, i],
);
if (isSelected) {
const siteId = siteList?.content[i]?.siteId;
if (!siteId) return;
handleRemoveFields(siteId);
return;
}
handleAppendFields(i);
};
const handleSelectAll = () => {
if (selectedRows.length === (siteList?.content.length ?? 0)) {
replace([]);
setSelectedRows([]);
} else {
replace([]);
setSelectedRows(siteList?.content.map((_, i) => i) ?? []);
siteList?.content.forEach((_, i) => handleAppendFields(i));
}
};
const handlePeriodCodeChange = (siteId: string) => (value: string) => {
const from = dayjs().format("YYYY-MM-DD 00:00:00");
const to = dayjs().add(Number(value), "day").format("YYYY-MM-DD 23:59:59");
setRememberPeriodCode((prev) => ({
...prev,
[siteId]: value,
}));
setRememberPeriodDate((prev) => ({
...prev,
[siteId]: { from, to },
}));
handleUpdateFields(siteId)({
pstgPrdSeCd: value,
pstgBgngDt: from,
pstgEndDt: to,
});
};
const handlePeriodDateChange =
(siteId: string) => (value: DateRange | undefined) => {
const from = dayjs(value?.from).format("YYYY-MM-DD 00:00:00");
const to = dayjs(value?.to).format("YYYY-MM-DD 23:59:59");
setRememberPeriodDate((prev) => ({
...prev,
[siteId]: { from, to },
}));
handleUpdateFields(siteId)({
pstgBgngDt: from,
pstgEndDt: to,
});
};
const handleSave = async () => {
if (await confirm("저장하시겠습니까?")) {
const data = form.getValues("pstg");
await savePosting(data);
}
};
return (
<Dialog onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
<Button color="emerald" disabled={!postingTargets.length}>
<i className="diveicon di-layer" />
사이트반영
</Button>
</DialogTrigger>
<DialogContent className="max-w-[68rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>사이트 반영</DialogTitle>
</DialogHeader>
<div className="card bg-accent !p-4">
<h3 className="font-semibold">
선택된 {POSTING_TYPE_LABEL[postingType]}
</h3>
<div className="mt-4 flex flex-wrap gap-3">
{postingTargets.map((target) => (
<Badge key={target.pstgTrgtNo}>
<span className="text-xl">{target.pstgTrgtNm}</span>
</Badge>
))}
</div>
</div>
<div className="m-2 max-h-[30rem] overflow-auto">
<Table variant="secondary">
<colgroup>
<col width="10%" />
<col width="25%" />
<col width="65%" />
</colgroup>
<TableHeader className="sticky top-0 z-10 bg-accent">
<TableRow>
<TableHead className="!px-0">
<Checkbox
checked={isAllSelected}
onCheckedChange={handleSelectAll}
/>
</TableHead>
<TableHead>사이트명</TableHead>
<TableHead>게시 구분</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{siteList?.content.map((site, i) => (
<TableRow key={site.siteId}>
<TableCell align="center" className="!px-0">
<Checkbox
checked={selectedRows.includes(i)}
onCheckedChange={() => handleSelectRow(i)}
/>
</TableCell>
<TableCell align="center">{site.siteNm}</TableCell>
<TableCell align="left">
<div className="flex items-center gap-4">
<PostingPeriodSelect
onValueChange={handlePeriodCodeChange(site.siteId)}
disabled={!selectedRows.includes(i)}
/>
{!!Number(rememberPeriodCode[site.siteId]) && (
<span
className={cn(
"text-lg text-label",
!selectedRows.includes(i) && "text-muted",
)}
>
게시 기간: <u>{postingPeriod(site.siteId)}</u>
</span>
)}
{Number(rememberPeriodCode[site.siteId]) === 0 && (
<DateRangePicker
handleDateChange={(date) =>
handlePeriodDateChange(site.siteId)(
date as DateRange | undefined,
)
}
disabled={!selectedRows.includes(i)}
/>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<DialogFooter className="!justify-center">
<Button size="lg" onClick={handleSave} disabled={!isFormValid}>
저장
</Button>
<DialogClose asChild>
<Button size="lg" color="white">
취소
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function PostingPeriodSelect({
...props
}: React.ComponentPropsWithoutRef<typeof Select>) {
return <CodeSelect upCd="PSTGPRDCD" {...props} />;
}
export { PostingDialog };
@@ -0,0 +1,2 @@
export { SiteApi } from "../api/site-service";
export type { Site } from "../model/site-interface";
@@ -0,0 +1,51 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import {
siteFormSchema,
type SiteService,
type SiteList,
type SiteDetail,
type SiteParams,
type SiteFormData,
} from "../model/site-interface";
export class SiteApiService implements SiteService {
public async getSiteList(params?: SiteParams): Promise<SiteList> {
const response = await client.get<SiteList>(
`${ENDPOINT.CMS_SERVICE.SITES}`,
{ params },
);
return response.data;
}
public async siteDetail(siteId: string): Promise<SiteDetail> {
const response = await client.get<SiteDetail>(
`${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`,
);
return response.data;
}
public async createSite(data: SiteFormData): Promise<void> {
const validatedData = siteFormSchema.parse(data);
const response = await client.post<void>(
`${ENDPOINT.CMS_SERVICE.SITES}`,
validatedData,
);
return response.data;
}
public async updateSite(siteId: string, data: SiteFormData): Promise<void> {
const validatedData = siteFormSchema.parse(data);
const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`,
validatedData,
);
return response.data;
}
public async deleteSite(siteId: string): Promise<void> {
await client.delete(`${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`);
}
}
export const SiteApi = new SiteApiService();
@@ -0,0 +1,7 @@
export { SiteApi } from "./api/site-service";
export {
siteFormSchema,
type SiteParams,
type Site,
type SiteFormData,
} from "./model/site-interface";
@@ -0,0 +1,59 @@
import { z } from "zod";
import type { PageableParams, PageableList } from "@/shared/model";
export interface SiteParams extends PageableParams {
siteNm?: string;
siteKndCd?: string;
useYn?: string;
}
export interface Site {
siteId: string;
siteNm: string;
siteKindCode: string;
bscSiteYn: string;
siteAddr: string;
useYn: string;
regDt: string;
rgTrNm: string;
dmnAddr: string;
}
export interface SiteDetail extends SiteFormData {
siteId: string;
siteKindNm: string;
basicSiteYn: string;
faxNumber: string;
regDt: string;
rgTr: string;
mdfcnDt: string;
mdFr: string;
rgTrNm: string;
mdfrNm: string;
}
export const siteFormSchema = z.object({
siteId: z.string().max(2).nonempty(),
siteNm: z.string().max(100).nonempty(),
siteExpln: z.string().max(1000).nullish(),
siteKndCd: z.string().max(10).nonempty(),
siteAddr: z.string().max(2000).nullish(),
faxNo: z.string().max(100).nullish(),
telNo: z.string().max(1000).nullish(),
lwndCn: z.string().max(2000).nullish(),
bscSiteYn: z.string(),
bkmkIcon: z.string().max(100).nullish(),
useYn: z.string(),
siteSkn: z.string().max(10).nullish(),
dmnAddr: z.string().nullish(),
});
export type SiteFormData = z.infer<typeof siteFormSchema>;
export type SiteList = PageableList<Site>;
export interface SiteService {
getSiteList(params: SiteParams): Promise<SiteList>;
siteDetail(siteId: string): Promise<SiteDetail>;
updateSite(siteId: string, data: SiteFormData): Promise<void>;
createSite(data: SiteFormData): Promise<void>;
}
@@ -0,0 +1 @@
export { LoginPage } from "./ui/login-page";
@@ -0,0 +1,129 @@
"use client";
import Image from "next/image";
import { images } from "@common/assets";
import { LoginApi } from "@/entities/auth";
import "./login.css";
function LoginPage() {
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const userId = formData.get("id") as string;
const password = formData.get("password") as string;
await LoginApi.login({ userId, password });
window.location.href = "/dashboard";
};
return (
<div className="login fx">
<div className="left_box fx">
<div className="text">
<h1>
하수도정보시스템
<br />
<span>CMS 통합관리시스템</span>
</h1>
<p>하수도정보시스템 CMS 통합관리시스템에 오신 것을 환영합니다.</p>
</div>
<div className="img">
<Image src={images.logo} width={229} height={46} alt="logo" />
</div>
</div>
<form
id="loginForm"
name="loginForm"
method="post"
onSubmit={handleSubmit}
>
<div className="login_box fx">
<ul className="fx">
<li>
<h2>ID</h2>
<div className="input_st st1">
<i className="diveicon di-account-box" />
<input
type="text"
className="id"
title="아이디를 입력하세요."
id="id"
name="id"
maxLength={12}
/>
</div>
</li>
<li>
<h2>PASSWORD</h2>
<div className="input_st st2">
<i className="diveicon di-key" />
<input
type="password"
className="input_style"
maxLength={25}
title="비밀번호를 입력하세요."
id="password"
name="password"
/>
</div>
</li>
</ul>
<div className="bottom_box fx">
<div className="checks fx">
<input
type="checkbox"
name="checkId"
title="로그인ID 저장여부"
id="checkId"
/>
<label htmlFor="checkId">아이디저장</label>
</div>
{/* <a href="javascript:fnUserSearch();" className=" pw_message fx"><i className="xi-external-link"></i>비밀번호 또는 아이디를 잃어버리셨나요?</a> */}
</div>
<button type="submit" className="login_btn fx">
<span>LOGIN</span>
</button>
<div className="footer">
<div className="copy fx">
<p>copyright © 2023 KECO. All rights reserved.</p>
</div>
<div className="fot_btn fx">
<a
href="https://keco.or.kr/web/lay1/S1T6C1348/contents.do"
target="_blank"
>
개인정보처리방침
</a>
<a
href="https://keco.or.kr/web/lay1/S1T6C1349/contents.do"
target="_blank"
>
저작권정책안내
</a>
<a
href="https://keco.or.kr/web/lay1/S1T6C1350/contents.do"
target="_blank"
>
이메일무단수집거부
</a>
</div>
<p className="addr fx">
<span>
주소 : (22689) 인천광역시 서구 환경로 42(경서동
종합환경연구단지)
</span>
<span>
전화 : (시스템) 032-590-4321, 3752, 4314 / (통 계) 032-590-3723,
3722
</span>
<span>팩스 :032-590-3729</span>
</p>
</div>
</div>
</form>
</div>
);
}
export { LoginPage };
+311
View File
@@ -0,0 +1,311 @@
/**
* OLYM Cascading Style Sheets
* File Name : login.css
* Description : 로그인 스타일시트
* Author : Web business Team / Choi Hyun Mi
* Email : desingtj@olym.co.kr
* Date : 2023.07.20
* Update : 2023.07.26
* Copyright(c) 2023 OLYM Communications. All Rights Reserved.
*/
html,
body {
background: #fff;
}
.fx {
display: flex;
-ms-display: flex;
-webkit-display: flex;
box-sizing: border-box;
}
.login {
width: 100%;
height: 100vh;
}
.login .left_box {
width: 100%;
background: url(../../../../../public/images/bg_login.jpg) no-repeat
center/cover;
flex-direction: column;
justify-content: space-between;
padding: 7% 5% 5% 5%;
}
.login .left_box .text h1 {
font-size: 53px;
color: #fff;
}
.login .left_box .text h1 span {
font-weight: 800;
}
.login .left_box .text p {
margin-top: 40px;
font-size: 18px;
color: #fff;
}
.login .login_box {
width: 100%;
flex-direction: column;
justify-content: center;
padding: 4% 12% 4% 6%;
}
.login .login_box > ul {
flex-direction: column;
gap: 40px;
}
.login .login_box > ul > li h2 {
font-size: 22px;
font-weight: 600;
color: #2640a5;
margin-bottom: 14px;
}
.login .login_box > ul > li .input_st {
position: relative;
}
.login .login_box > ul > li .input_st .diveicon {
position: absolute;
top: 50%;
left: 28px;
transform: translateY(-50%);
font-family: "Xeicon";
font-size: 25px;
color: #003288;
}
.login .login_box > ul > li input {
width: 100%;
padding: 20px 20px 20px 70px;
background: #f7f7f7;
box-sizing: border-box;
border: none;
border-radius: 5px;
font-size: 18px;
font-weight: 700;
color: #000;
height: auto;
}
.login .login_box > ul > li input:focus {
outline: 1px solid #3c5fe5;
}
.login .login_box > ul > li input::placeholder {
color: #afabab;
font-weight: 500;
}
.login .login_box .bottom_box {
justify-content: space-between;
gap: 20px;
padding: 50px 0 45px 0;
}
.login .login_box .bottom_box .checks {
display: flex;
align-items: center;
gap: 8px;
}
.login .login_box .bottom_box .checks input[type="checkbox"] {
width: 18px;
height: 18px;
}
.login .login_box .bottom_box .checks label {
font-size: 14px;
}
.login .login_box .bottom_box .pw_message {
align-items: center;
gap: 5px;
color: #636363;
font-weight: 500;
border-bottom: 1px solid #ccc;
padding-bottom: 5px;
}
.login .login_box .bottom_box .pw_message i {
color: #327af5;
font-size: 20px;
}
.login .login_box .login_btn {
position: relative;
border-radius: 5px;
padding: 24px 15px;
background: linear-gradient(
to left,
#1d3969 0%,
#264078 10%,
#172558 40%,
#162254 50%,
#111c43 60%,
#111c42 100%
);
overflow: hidden;
justify-content: center;
align-items: center;
gap: 10px;
color: #fff;
cursor: pointer;
}
.login .login_box .login_btn span {
font-family: "Montserrat";
font-size: 19px;
font-weight: 700;
position: relative;
}
.login .login_box .login_btn i {
font-size: 22px;
transform: translateY(-2px);
}
.login .login_box .login_btn:before {
display: block;
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
background: linear-gradient(
to right,
#1d3969 0%,
#264078 10%,
#172558 40%,
#162254 50%,
#111c43 60%,
#111c42 100%
);
transition: opacity 0.5s;
}
.login .login_box .login_btn:hover::before {
opacity: 1;
}
.login .footer .copy {
margin-top: 36px;
justify-content: space-between;
align-items: center;
gap: 3%;
}
.login .footer .copy p {
font-size: 13px;
color: #d4d4d4;
width: 44%;
text-align: center;
}
.login .footer .copy::before,
.login .login_box .copy::after {
content: "";
width: 20%;
height: 1px;
background: #d4d4d4;
}
.login .footer .fot_btn {
gap: 20px;
margin: 30px 0 10px 0;
}
.login .footer .fot_btn a {
color: #333;
font-weight: 700;
font-size: 13px;
}
.login .footer .fot_btn a:hover {
color: #003288;
}
.login .footer .addr {
color: #666;
font-size: 13px;
gap: 3px 20px;
flex-wrap: wrap;
}
/* 비밀번호 변경 팝업창 */
.login .modal .modal_container {
width: 750px;
}
.login .pw_desc {
padding: 10px 10px 30px 10px;
color: #666;
}
.login .pw_desc p {
font-size: 1.25rem;
color: #000;
font-weight: 700;
margin-bottom: 10px;
}
.login .pw_desc span {
color: #f60;
font-weight: 700;
}
.login .modal .form_write {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
padding: 20px 50px;
}
@media (max-width: 1024px) {
.login {
flex-direction: column;
height: auto;
}
.login .left_box {
padding: 30px;
flex-direction: column-reverse;
gap: 20px;
}
.login .left_box .text h1 {
font-size: 30px;
line-height: 1.2em;
}
.login .left_box .text p {
margin-top: 20px;
font-size: 14px;
}
.login .left_box .img img {
height: 40px;
}
.login .login_box {
padding: 30px;
}
.login .login_box > ul {
gap: 20px;
}
.login .login_box > ul > li h2 {
margin-bottom: 7px;
}
.login .login_box > ul > li .input_st::after {
left: 15px;
font-size: 20px;
}
.login .login_box > ul > li input {
padding: 15px 10px 15px 50px;
font-size: 16px;
}
.login .login_box .bottom_box {
flex-direction: column;
align-items: flex-start;
padding: 30px 0;
}
.login .login_box .bottom_box .id_save label {
font-size: 14px;
}
.login .login_box .login_btn {
padding: 15px;
}
.login .login_box .login_btn span {
font-size: 16px;
}
.login .login_box .login_btn i {
font-size: 18px;
transform: translateY(-1px);
}
.login .login_box .copy p {
width: 100%;
}
.login .login_box .copy::before,
.login .login_box .copy::after {
display: none;
}
.login .footer .addr {
font-size: 11px;
gap: 3px 10px;
}
.login .modal .modal_container {
width: 90%;
}
.login .pw_desc {
padding: 0 0 20px 0;
}
.login .modal .form_write {
padding: 10px;
}
}
/* // login */
@@ -0,0 +1 @@
export { BoardPage } from "./ui/board-page";
@@ -0,0 +1,5 @@
function BoardPage() {
return <div>게시판 관리</div>;
}
export { BoardPage };
@@ -0,0 +1,8 @@
import { Client } from "@common/network";
import { BASE_URL } from "../config/api";
import { SITE_ID } from "../config/env";
export const client = Client.getInstance({
baseURL: BASE_URL,
siteId: SITE_ID,
});
@@ -0,0 +1 @@
export { client } from "./client";
@@ -0,0 +1,40 @@
import { PROXY_HOST } from "./env";
export const BASE_URL = `${PROXY_HOST}/server`;
export const ENDPOINT = {
ATTACH_SERVICE: {
TUS_UPLOAD: "/attach-service/api/v1/tus/upload",
EDITOR_UPLOAD: "/attach-service/api/v1/upload/editor",
IMAGES: "/attach-service/api/v1/images",
ATTACHMENTS: "/attach-service/api/v1/atfl/attachments",
},
USER_SERVICE: {
LOGIN: "/user-service/login",
USERS: "/user-service/api/v1/users",
SILENT_REFRESH: "/user-service/api/v1/users/token/refresh",
AUTHRTS: "/user-service/api/v1/authorizations",
ROLES_AUTHRTS: "/user-service/api/v1/role-authorizations",
},
CMS_SERVICE: {
SITES: "/cms-service/api/v1/sites",
IPS: "/cms-service/api/v1/site-ips",
DOMAINS: "/cms-service/api/v1/site-dmns",
POSTINGS: "/cms-service/api/v1/site-pstgs",
CODE: "/cms-service/api/v1/code/cache",
MENUS: "/cms-service/api/v1/menus",
MENU_ROLES: "/cms-service/api/v1/menu-roles",
MENU_AUTHRTS: "/cms-service/api/v1/menu-authrts",
CONTENTS: "/cms-service/api/v1/contents",
USER_ORGANIZATION: "/cms-service/api/v1/user-ognz",
IMAGES: "/cms-service/api/v1/images",
BANNERS: "/cms-service/api/v1/bnrs",
TEMPLATE: "/cms-service/api/v1/tmplt",
TERMS: "/cms-service/api/v1/trms",
},
BOARD_SERVICE: {
BBS: "/board-service/api/v1/bbs",
},
};
export const JWT_EXPIRATION_TIME = 1800000;
@@ -0,0 +1,13 @@
export const PORT = process.env.NEXT_PUBLIC_PORT || "3000";
export const PROXY_HOST =
process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`;
export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL;
export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "CS";
export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
export const ACCESS_TOKEN =
process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token";
export const REFRESH_TOKEN =
process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token";
export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
@@ -0,0 +1,2 @@
export * from "./api";
export * from "./env";
@@ -0,0 +1,29 @@
import Cookies from "js-cookie";
export function getCookie(name: string): string | undefined {
if (typeof window === "undefined") {
return undefined;
}
return Cookies.get(name);
}
export function setCookie(
name: string,
value: string,
options?: Cookies.CookieAttributes,
): void {
if (typeof window === "undefined") {
return;
}
Cookies.set(name, value, options);
}
export function removeCookie(
name: string,
options?: Cookies.CookieAttributes,
): void {
if (typeof window === "undefined") {
return;
}
Cookies.remove(name, options);
}
@@ -0,0 +1,3 @@
export { getCookie, setCookie, removeCookie } from "./cookie";
export { useMenuStore } from "./use-menu-store";
export { useUserStore } from "./use-user-store";
@@ -0,0 +1,14 @@
import { useStore } from "zustand";
import { useContext } from "react";
import type { AlertStore } from "../store/alert-store";
import { StoreContext } from "../providers/store-provider";
export const useAlertStore = <T>(selector: (store: AlertStore) => T): T => {
const storeContext = useContext(StoreContext);
if (!storeContext) {
throw new Error("useAlertStore must be used within StoreProvider");
}
return useStore(storeContext.alertStore, selector);
};
@@ -0,0 +1,14 @@
import { useStore } from "zustand";
import { useContext } from "react";
import type { AttachmentStore } from "../store/attachment-store";
import { StoreContext } from "../providers/store-provider";
export const useAttachmentStore = <T>(
selector: (store: AttachmentStore) => T,
): T => {
const store = useContext(StoreContext);
if (!store) {
throw new Error("useAttachmentStore must be used within StoreProvider");
}
return useStore(store.attachmentStore, selector);
};
@@ -0,0 +1,14 @@
import { useStore } from "zustand";
import { useContext } from "react";
import type { MenuStore } from "../store/menu-store";
import { StoreContext } from "../providers/store-provider";
export const useMenuStore = <T>(selector: (store: MenuStore) => T): T => {
const storeContext = useContext(StoreContext);
if (!storeContext) {
throw new Error("useMenuStore must be used within StoreProvider");
}
return useStore(storeContext.menuStore, selector);
};
@@ -0,0 +1,64 @@
import { useEffect, useState } from "react";
import { type HttpResponse, Upload } from "tus-js-client";
import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "../config";
import { useAttachmentStore } from "./use-attachment-store";
import { getCookie } from "./cookie";
export const useTus = () => {
const { attachmentFileCode, setAttachmentFileCode } = useAttachmentStore(
(state) => state,
);
const [file, setFile] = useState<File | null>(null);
const [upload, setUpload] = useState<Upload | null>(null);
const [response, setResponse] = useState<HttpResponse | null>(null);
const [progress, setProgress] = useState<number>(0);
const metadata = {
filename: file?.name ?? "",
filetype: file?.type ?? "",
atflCd: attachmentFileCode,
};
const createUpload = () => {
return new Upload(file!, {
endpoint: `${ENDPOINT.ATTACH_SERVICE.TUS_UPLOAD}`,
chunkSize: 10 * 1024 * 1024,
headers: {
[CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "",
[AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "",
},
metadata,
onError(error) {
throw new Error(`Failed because: ${error}`);
},
onProgress(bytesUploaded, bytesTotal) {
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
setProgress(Number(percentage));
},
onAfterResponse(req, res) {
const fileInfo = res.getHeader("X-File-Info");
if (fileInfo === undefined || fileInfo === null) return;
setAttachmentFileCode(JSON.parse(fileInfo)?.atflCd ?? "");
setResponse(res);
},
});
};
useEffect(() => {
if (!file) return;
const upload = createUpload();
upload.start();
setUpload(upload);
}, [file]);
return {
file,
upload,
response,
progress,
setFile,
};
};
@@ -0,0 +1,14 @@
import { useStore } from "zustand";
import { useContext } from "react";
import type { UserStore } from "../store/user-store";
import { StoreContext } from "../providers/store-provider";
export const useUserStore = <T>(selector: (store: UserStore) => T): T => {
const storeContext = useContext(StoreContext);
if (!storeContext) {
throw new Error("useUserStore must be used within StoreProvider");
}
return useStore(storeContext.userStore, selector);
};
@@ -0,0 +1,34 @@
interface SortConfig {
empty: boolean;
sorted: boolean;
unsorted: boolean;
}
interface PageableConfig {
sort: SortConfig;
offset: number;
pageNumber: number;
pageSize: number;
paged: boolean;
unpaged: boolean;
}
export interface PageableList<T> {
content: T[];
pageable: PageableConfig;
sort: SortConfig;
totalPages: number;
totalElements: number;
number: number;
numberOfElements: number;
size: number;
first: boolean;
last: boolean;
empty: boolean;
}
export interface PageableParams {
page?: number;
size?: number;
sort?: string[];
}
@@ -0,0 +1 @@
export * from "./api-interface";
@@ -0,0 +1 @@
export { StoreProvider } from "./store-provider";
@@ -0,0 +1,38 @@
"use client";
import { createContext, useRef } from "react";
import {
createAlertStore,
createAttachmentStore,
createMenuStore,
createUserStore,
} from "../store";
type StoreContextType = {
alertStore: ReturnType<typeof createAlertStore>;
attachmentStore: ReturnType<typeof createAttachmentStore>;
menuStore: ReturnType<typeof createMenuStore>;
userStore: ReturnType<typeof createUserStore>;
};
export const StoreContext = createContext<StoreContextType | undefined>(
undefined,
);
export const StoreProvider = ({ children }: { children: React.ReactNode }) => {
const storeRef = useRef<StoreContextType | null>(null);
if (storeRef.current === null) {
storeRef.current = {
alertStore: createAlertStore(),
attachmentStore: createAttachmentStore(),
menuStore: createMenuStore(),
userStore: createUserStore(),
};
}
return (
<StoreContext.Provider value={storeRef.current}>
{children}
</StoreContext.Provider>
);
};
@@ -0,0 +1,25 @@
import { createStore } from "zustand/vanilla";
export type AlertState = {
message: string;
callback?: () => void;
};
export type AlertActions = {
setMessage: (message: string, callback?: () => void) => void;
};
export type AlertStore = AlertState & AlertActions;
export const defaultInitState: AlertState = {
message: "",
callback: undefined,
};
export const createAlertStore = (initState: AlertState = defaultInitState) => {
return createStore<AlertStore>()((set) => ({
...initState,
setMessage: (message: string, callback?: () => void) =>
set(() => ({ message, callback })),
}));
};
@@ -0,0 +1,25 @@
import { createStore } from "zustand/vanilla";
export type AttachmentState = {
attachmentFileCode: string;
};
export type AttachmentActions = {
setAttachmentFileCode: (attachmentFileCode: string) => void;
};
export type AttachmentStore = AttachmentState & AttachmentActions;
export const defaultInitState: AttachmentState = {
attachmentFileCode: "",
};
export const createAttachmentStore = (
initState: AttachmentState = defaultInitState,
) => {
return createStore<AttachmentStore>()((set) => ({
...initState,
setAttachmentFileCode: (attachmentFileCode: string) =>
set({ attachmentFileCode }),
}));
};
@@ -0,0 +1,4 @@
export { createAlertStore } from "./alert-store";
export { createAttachmentStore } from "./attachment-store";
export { createMenuStore } from "./menu-store";
export { createUserStore, type IUserInfo } from "./user-store";
@@ -0,0 +1,23 @@
import { createStore } from "zustand/vanilla";
import type { MenuItem } from "@common/components/api";
export type MenuState = {
currentMenuInfo: MenuItem | null;
};
export type MenuActions = {
setCurrentMenuInfo: (menu: MenuItem) => void;
};
export type MenuStore = MenuState & MenuActions;
export const defaultInitState: MenuState = {
currentMenuInfo: null,
};
export const createMenuStore = (initState: MenuState = defaultInitState) => {
return createStore<MenuStore>()((set) => ({
...initState,
setCurrentMenuInfo: (menu: MenuItem) => set({ currentMenuInfo: menu }),
}));
};
@@ -0,0 +1,56 @@
import { createStore } from "zustand/vanilla";
export interface IUserInfo {
regDt: string;
mdfcnDt: string;
rgtr: string;
mdfr: string;
userUnqId: string;
userId: string;
userName: string;
email: string;
encryptedPassword: string;
refreshToken: string;
userStateCode: string;
lastLoginDate: string;
loginFailCount: number;
pswdHnt: string;
pswdAns: string;
mphn: string;
telno: string;
zip: string;
addr: string;
daddr: string;
laeSeCd: string;
ogdpSeCd: string;
aprvYn: string;
autzrId: string;
expryYmd: string;
lastPswdChgDt: string;
googleId: string;
kakaoId: string;
naverId: string;
isSocialUser: boolean;
hasPassword: boolean;
}
export type UserState = {
userInfo: IUserInfo | null;
};
export type UserActions = {
setUserInfo: (userInfo: IUserInfo | null) => void;
};
export type UserStore = UserState & UserActions;
export const defaultInitState: UserState = {
userInfo: null,
};
export const createUserStore = (initState: UserState = defaultInitState) => {
return createStore<UserStore>()((set) => ({
...initState,
setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })),
}));
};
@@ -0,0 +1,32 @@
import {
Dialog,
DialogContent,
DialogFooter,
DialogClose,
Button,
} from "@common/components/ui";
import { useAlertStore } from "../lib/use-alert-store";
function AlertDialog() {
const { message, callback, setMessage } = useAlertStore((state) => state);
const handleOpenChange = () => {
setMessage("");
callback?.();
};
return (
<Dialog open={!!message} onOpenChange={handleOpenChange}>
<DialogContent aria-describedby={undefined}>
<div className="py-10">
<p className="text-center text-[1.4rem] text-foreground">{message}</p>
</div>
<DialogFooter className="!justify-center">
<DialogClose asChild>
<Button>확인</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export { AlertDialog };
@@ -0,0 +1,46 @@
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Dialog,
DialogContent,
DialogFooter,
DialogClose,
Button,
type ButtonProps,
} from "@common/components/ui";
type ConfirmDialogProps = {
open?: boolean;
confirmButtonProps?: ButtonProps & { label?: string };
cancelButtonProps?: ButtonProps & { label?: string };
};
function ConfirmDialog({
open,
confirmButtonProps,
cancelButtonProps,
}: ConfirmDialogProps) {
const { message, isOpen, onConfirm, onCancel } = useConfirm();
return (
<Dialog open={open ?? isOpen} onOpenChange={onCancel}>
<DialogContent aria-describedby={undefined}>
<div className="py-10">
<p className="text-center text-[1.4rem] text-foreground">{message}</p>
</div>
<DialogFooter className="!justify-center">
<DialogClose asChild>
<Button onClick={onConfirm} {...confirmButtonProps}>
{confirmButtonProps?.label ?? "확인"}
</Button>
</DialogClose>
<DialogClose asChild>
<Button onClick={onCancel} color="white" {...cancelButtonProps}>
{cancelButtonProps?.label ?? "취소"}
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export { ConfirmDialog };
+175
View File
@@ -0,0 +1,175 @@
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import {
Controller,
FormProvider,
useFormContext,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form";
import { cn, Label } from "@common/components";
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue,
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue,
);
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
);
});
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
);
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
});
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-[0.8rem] text-muted-foreground", className)}
{...props}
/>
);
});
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message ?? "") : children;
if (!body) {
return null;
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-[1rem] font-medium text-destructive", className)}
{...props}
>
{body}
</p>
);
});
FormMessage.displayName = "FormMessage";
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
};
@@ -0,0 +1,22 @@
import { PageTitle } from "@common/components";
import { useMenuStore } from "../lib/use-menu-store";
function PageLayout({ children }: { children: React.ReactNode }) {
const { currentMenuInfo } = useMenuStore((state) => state);
const menuPaths = currentMenuInfo?.menuPath
?.split(">")
.map((path) => path.trim());
return (
<div className="h-auto min-h-full bg-container pb-2">
<PageTitle
title={currentMenuInfo?.menuKornNm ?? ""}
menuPaths={menuPaths ?? []}
/>
{children}
</div>
);
}
export { PageLayout };
@@ -0,0 +1,12 @@
import { Skeleton } from "@common/components/ui/skeleton";
function SkeletonPage() {
return (
<div className="flex flex-col gap-4 p-6">
<Skeleton className="h-[6rem] rounded-xl" />
<Skeleton className="h-[60rem] rounded-xl" />
</div>
);
}
export { SkeletonPage };
@@ -0,0 +1 @@
export { FormWrapper } from "./ui/form-wrapper";
@@ -0,0 +1,68 @@
import { useRouter } from "next/navigation";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { type FieldValues, type UseFormReturn } from "react-hook-form";
import { Button } from "@common/components/ui";
import { Form } from "@/shared/ui/form";
type FormWrapperProps<T extends FieldValues> = {
form: UseFormReturn<T>;
children: React.ReactNode;
isModifyMode?: boolean;
handleSave: (data: T) => Promise<void>;
handleDelete?: () => Promise<void>;
};
function FormWrapper<T extends FieldValues>({
form,
children,
isModifyMode,
handleSave,
handleDelete,
}: FormWrapperProps<T>) {
const router = useRouter();
const { confirm } = useConfirm();
const onSubmit = async (data: T) => {
const confirmMessage = isModifyMode
? "수정하시겠습니까?"
: "저장하시겠습니까?";
if (await confirm(confirmMessage)) {
await handleSave(data);
}
};
const onDelete = async () => {
if (await confirm("삭제하시겠습니까?")) {
await handleDelete?.();
}
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
{children}
<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={onDelete}>
삭제
</Button>
)}
</div>
</form>
</Form>
);
}
export { FormWrapper };
+19
View File
@@ -0,0 +1,19 @@
import type { Config } from "tailwindcss";
import tailwindcssAnimate from "tailwindcss-animate";
import presetConfig from "../../../tailwind.config.preset";
export default {
presets: [presetConfig],
content: [
"./src/**/*.{js,ts,jsx,tsx,mdx}",
"../../../packages/common/components/src/ui/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
boxShadow: {
blur: "0 0 10px -1px rgba(0, 0, 0, 0.06)",
},
},
},
plugins: [tailwindcssAnimate],
} satisfies Config;
+48
View File
@@ -0,0 +1,48 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": "./src",
"paths": {
"@/*": ["./*"],
"@common/components/*": ["../../../../packages/common/components/src/*"],
"@common/components": ["../../../../packages/common/components/src"],
"@common/network/*": ["../../../../packages/common/network/*"],
"@common/network": ["../../../../packages/common/network/"]
}
},
"include": [
".eslintrc.*",
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.mts",
"**/*.js",
"**/*.cjs",
"**/*.mjs",
"**/*.jsx",
"**/*.json",
".next/types/**/*.ts"
],
"exclude": ["**/node_modules", "**/.*/*"]
}
-2
View File
@@ -1,2 +0,0 @@
NEXT_PUBLIC_PORT=3000
NEXT_PUBLIC_SERVER_API_URL=http://192.168.0.102:8000
+2
View File
@@ -36,3 +36,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
.env*

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