Compare commits

..
Author SHA1 Message Date
rsone001 80036ed876 feat: 펌프장 검색 추가 2026-03-17 10:03:35 +09:00
rsone001 d225e9fcac refactor: 05.펌프장 머지셀 변경 2026-03-11 11:11:04 +09:00
rsone001 de6bb04927 refactor: 02.하수관로 표준화 데이터 변경 2026-03-11 11:03:53 +09:00
rsone001 5f639a2fa9 refactor: 03. 하수관로 유지관리 표준화 데이터 변경 2026-03-11 10:58:06 +09:00
rsone001 d8c908b68e refactor: 04.하수저류시설 및 빗물펌프장 2026-03-11 10:56:20 +09:00
rsone001 3a1b22b57b refactor:05.펌프장 표준화 데이터로 변경 2026-03-11 10:52:27 +09:00
rsone001 f3ba3d9b31 st: fix: 불필요 코드 삭제 2026-03-09 11:57:23 +09:00
rsone001 6b92d0630a Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-03-06 13:04:21 +09:00
rsone001 6b4dd6aba7 st: feat: 일반검증 컴포넌트 구현중 2026-03-06 13:04:14 +09:00
chanii8 0556245d62 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-03-06 11:42:58 +09:00
chanii8 0a52b6c507 [st] feat: 이상건수 상세 다이얼로그 수정 2026-03-06 11:42:53 +09:00
rsone001 727d448dc5 st: fix: db표준화 작업에 맞춰 수정 2026-03-06 10:37:50 +09:00
chanii8 90ebabd5bd [st] feat: 검증 대상 목록 수정 2026-03-05 17:47:59 +09:00
chanii8 a5aa6bfdf4 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-03-05 16:27:11 +09:00
chanii8 0292353c24 docs: IIPBS 단어 목록 문서 추가 2026-03-05 16:26:57 +09:00
chanii8 9eb0cd428b [st] feat: 검증 이력 '검증일시'로 조회하는 기능 추가 2026-03-05 16:22:30 +09:00
chanii8 3fed584ec4 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-03-05 14:10:21 +09:00
chanii8 451d25666a [st] feat: 검증 이력 테이블 수정 2026-03-05 14:10:15 +09:00
rsone001 63afdfc726 st: fix: db 표준화 작업에 맞게 interface 및 type 수정, middleware 수정 2026-03-05 10:10:56 +09:00
chanii8 64fcc423b5 [st] chore: 검증 이력 그리드 컬럼 수정 2026-03-04 17:17:40 +09:00
chanii8 c72b2c3f58 [st] feat: 이상건수 다이얼로그 작업 2026-03-04 16:22:21 +09:00
chanii8 fe3e6de085 [st] feat: H-B 통계 검증 페이지 API 연결 작업 2026-03-04 15:13:07 +09:00
chanii8 ff5b43e1d2 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-03-04 11:36:03 +09:00
rsone001 c957c2ff8d st: feat: 그리드 해더 색상 통일, 통계입력현황 조회 기능 추가 & 검색폼 컴포넌트 로직 수정(통계입력현황 조회시 도를 제외한 시는 행정구역(sggNm)을 조회하지 않도록) 2026-02-26 15:13:56 +09:00
chanii8 f0dcccaee9 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-02-26 13:05:10 +09:00
mind-one005 cf825ef3ad Merge branch 'iipbs/dm/dev' 2026-02-26 13:01:37 +09:00
mind-one005 a85fb848f7 dm_new: eslint 설정 수정 2026-02-26 12:57:03 +09:00
chanii8 d3bd1b5721 fix: glob 에러 2026-02-26 11:55:09 +09:00
chanii8 485b041f97 IIPBS 프로젝트 템플릿 생성 2026-02-26 11:43:45 +09:00
dongsilove 55aaee8983 dm_new: fix 2026-02-25 21:46:52 +09:00
dongsilove c6cf6e90c3 dm_new: ImgCard 주석처리 2026-02-25 21:44:27 +09:00
dongsilove 8c663fa4e8 Merge remote-tracking branch 'origin/main' into iipbs/sn/dev 2026-02-25 20:22:11 +09:00
mind-one005 b6ca669c2e dm_new: 우편번호 추가 2026-02-25 18:15:25 +09:00
LGS 5e1a5c68c8 물 재이용 대민 서비스 수요신청 > 수요신청현황 수정버튼 추가 및 이미지 교체 2026-02-25 17:13:26 +09:00
rsone001 690bca4c06 st: fix: 하수관로 검토 컬럼 수정 & 관리자 계정 판별 로직 추가 2026-02-25 17:04:27 +09:00
rsone001 5600ce01bd st: fix: 그리드 mergeCell sort 오류 & ESLint 에러 수정 2026-02-25 16:13:48 +09:00
rsone001 93e652a557 Merge remote-tracking branch 'origin/feature/review-skeleton-sein' into iipbs/st/dev 2026-02-25 14:42:10 +09:00
rsone001 cc3c5c5d68 st: feat: 하수도보급률 입력 저장, 체출 로직 작업, 도-시 조회 시 버튼 조건 추가 2026-02-25 14:39:40 +09:00
rsone001 2a473c85db refactor: 03.하수관로 유지관리 시설명 검색창 제거 2026-02-25 13:39:03 +09:00
rsone001 1a5b000b02 03.하수관로 유지관리 tab1. 개보수 표준화 데이터 조회 2026-02-25 13:34:56 +09:00
chanii8 2b76e21de5 [st] design: 탭 디자인 수정 2026-02-25 11:09:21 +09:00
dongsilove f812e021b4 dm_new: 우편번호 검색 수정 2026-02-24 22:37:52 +09:00
mind-one005 4cfe33bdc7 Merge branch 'iipbs/dm/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/dm/dev 2026-02-24 18:05:23 +09:00
mind-one005 25a53f8162 dm_new 우편번호 검색 2026-02-24 18:05:17 +09:00
mind-one005 4f0a8374e7 dm_new: 우편번호 검색 2026-02-24 18:04:17 +09:00
rsone001 f402961de7 refactor: 02.하수관로 tab2.시설별 설치현황 표준화 데이터 매핑 수정 2026-02-24 17:19:16 +09:00
rsone001 ec250bded1 refactor: 02.하수관로 tab1.지역별 시설현황 표준화 데이터 매핑 수정 2026-02-24 16:23:47 +09:00
rsone001 8e7baee930 refactor: 01. 하수도 보급률 불필요컬럼 제거 및 컬럼 수정 2026-02-24 11:42:16 +09:00
LGS c30bbbd263 물 재이용 대민서비스 수요신청 수요정보등록 후보지조회 팝업창 신규 추가 및 물재이용소개/물재이용현황 이미지교체 2026-02-24 10:57:26 +09:00
mind-one005 55c953d284 fo: excelDown시 조건추가 2026-02-23 18:07:01 +09:00
rsone001 c2de582b76 refactor: 01.하수도보급률 표준화 데이터 매칭 수정 2026-02-23 17:08:56 +09:00
LGS 8d0a79063e 대민서비스 물재이용 랜딩 페이지 수요처등록 우편번호 검색 팝업창 추가 2026-02-23 14:19:47 +09:00
rsone001 f51f6e58d8 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-02-23 10:03:44 +09:00
rsone001 103efe79d3 st: feat: 진행상황 정보 표시 2026-02-23 10:03:37 +09:00
mind-one005 cd392b0f11 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-02-23 10:01:57 +09:00
mind-one005 019bfaf9c2 dm_new merge 2026-02-23 10:01:49 +09:00
mind-one005 a0e9d5dcab Merge branch 'iipbs/dm/dev' into iipbs/sn/dev 2026-02-23 09:46:25 +09:00
mind-one005 acdeb9af6e dm_new: topNav 로그인 상태 수정 2026-02-23 09:42:32 +09:00
chanii8 2e8163e593 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-02-21 12:04:43 +09:00
chanii8andCursor 184e031ec9 chore: pre-push hook을 변경된 패키지만 빌드하도록 최적화
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 11:56:48 +09:00
chanii8andCursor ef26a73af6 chore: .cursor 폴더 Git 추적에서 제거
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 10:37:03 +09:00
chanii8andCursor 64f4194947 chore: gitignore 파일 복원
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 10:35:47 +09:00
chanii8andCursor 14f534c3fc test: husky pre-push hook 설정
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 09:53:40 +09:00
chanii8 e3164fc96d [st] fix: eslint, typescript 에러 2026-02-20 17:48:46 +09:00
mind-one005 46c1d33750 dm_new: 수요처등록수정 2026-02-20 17:31:46 +09:00
chanii8 1bc0685289 [st] feat: 검증 항목 관리 IMR 탭 작업 2026-02-20 17:30:24 +09:00
mind-one005 b0baff824b Merge branch 'iipbs/dm/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/dm/dev 2026-02-20 16:48:39 +09:00
mind-one005 baa42bd669 dm_new: 디자인 수정 2026-02-20 16:48:34 +09:00
LGS cec93cf77e 대민서비스 물재이용 랜딩 페이지 수요정보등록 페이지 요구시기 데이트피커 달력 아이콘 svg 속성 오류 수정 2026-02-20 16:43:57 +09:00
rsone001 8fa00797c8 refactor: tab key 지정된 코드로 변경 2026-02-20 16:10:39 +09:00
LGS a154a11ed0 대민서비스 물재이용 랜딩 페이지 서브 타이틀 라인 UI 수정 및 기타 수정 2026-02-20 16:04:17 +09:00
rsone001 cb29ac6c4d refactor: 컬럼.ts- editable: false, styleName: "rg-cell-readonly" 추가 2026-02-20 15:22:09 +09:00
rsone001 b6a75f6ee0 feat: 05.펌프장 조회 기능 추가 2026-02-20 14:02:27 +09:00
LGS 76fdba2bc9 대민서비스 물재이용 랜딩 페이지 최소 해상도 1800 사이즈 고정처리 2026-02-20 11:36:18 +09:00
dongsilove 17a5c20266 dm_new: 로그인 처리 중 2026-02-19 23:47:56 +09:00
mind-one005 d55b7e384e Merge branch 'iipbs/dm/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/dm/dev 2026-02-19 18:12:39 +09:00
mind-one005 f4de1a6027 dm_new: 기타 수정 2026-02-19 18:12:34 +09:00
LGS 92ed4fccc8 대민서비스 물재이용 랜딩 페이지(물재이용사례 - 해외재이용사례 서브페이지 추가) 2026-02-19 17:02:50 +09:00
rsone001 842ec36c32 feat: 하수 저류시설 및 빗물 펌프장 조회기능 추가 2026-02-19 15:41:19 +09:00
100JM 433ef5e24d link data from 스키마 수정 2026-02-19 15:38:47 +09:00
chanii8 a61b3905d4 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-02-19 15:23:26 +09:00
mind-one005 640d4cb1a8 dm_new: Lnb 제목 외 2026-02-19 14:54:30 +09:00
rsone001 4ff9b8e793 st: refactor: editable: false 항목 cell 스타일 적용 방식 수정 2026-02-19 11:51:15 +09:00
rsone001 00adb28aa1 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-02-19 11:43:28 +09:00
chanii8 d474af5688 [st] feat: 검증 항목 상세 폼 유효성 작업 2026-02-19 10:50:04 +09:00
rsone001 4a74cd0394 st: fix: 하수도보급률 저장, 일반 검증 결과 체크 2026-02-19 10:45:56 +09:00
LGS 8826340780 대민서비스 물재이용 랜딩 페이지(물재이용사례 - 사례개요 및 메인화면 "바로가기" 서브페이지 라우팅 연결) 2026-02-19 10:33:05 +09:00
chanii8 f31826da5a [st] design: DataGrid 셀 높이 조절 2026-02-19 10:24:33 +09:00
chanii8 0149f489a7 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-02-19 09:34:30 +09:00
LGS b67f4828b1 Merge commit '7a91fcbd8042d4b8518583a1687a5857a4ded163' into iipbs/dm/dev 2026-02-19 09:32:19 +09:00
dongsilove 7a91fcbd80 dm_new menu 2026-02-18 22:11:12 +09:00
rsone001 be21ea3e8a feat: 03.하수관로 유지관리 tab2관로준설 2026-02-16 17:58:41 +09:00
rsone001 4b883a30c5 feat: 03.하수관로 유지관리 tab1 조회기능 2026-02-16 17:47:28 +09:00
rsone001 37e3b6ebdb feat: 02.하수관로 tab4관경별 현황 조회 기능 2026-02-16 16:33:25 +09:00
rsone001 ca83ac22aa feat: 02.하수관로 tab3관종별 현황 2026-02-16 15:03:38 +09:00
dongsilove f621f4dfa1 Merge remote-tracking branch 'origin/main' into iipbs/dm/dev 2026-02-14 15:21:57 +09:00
dongsilove 5dc6fe4e3b wr: 엑셀 버튼명 2026-02-13 22:32:24 +09:00
dongsilove f2a71893d4 wr: 항목 표출 변경 2026-02-13 21:17:17 +09:00
dongsilove a91bcd9cf7 wr: 시군구 초기화 2026-02-13 20:21:06 +09:00
mind-one005 d9baf33a63 dm_new 2026-02-13 16:43:19 +09:00
chanii8 b644281348 [st] feat: 검증 항목 상세 다이얼로그 작업 2026-02-13 14:57:20 +09:00
chanii8 1572711749 [common/components] feat: Header 컴포넌트 중앙 네비게이션 ScrollArea 적용 2026-02-13 14:16:18 +09:00
dongsilove 62fef6bcdc wr: 버튼 크기 통일 2026-02-12 23:48:44 +09:00
dongsilove 60edf0a1ff wr: 시설등록, 실적등록 버튼 수정 2026-02-12 23:02:46 +09:00
mind-one005 36ce09b754 dm_new 추가 2026-02-12 18:18:08 +09:00
LGS d4775ff745 대민서비스 물재이용 랜딩 페이지(수요신청 외 서브페이지 추가) 2026-02-12 17:41:36 +09:00
chanii8 93d4cb57e6 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-02-12 17:00:45 +09:00
chanii8 a256379633 [st] feat: 검증 항목 관리 페이지 작업 2026-02-12 16:54:07 +09:00
rsone001 2873d1b0b9 st: fix & refactor: TabDataGrid에서 텝 전환시 컬럼 포맷 및 footer 사라지는 오류 수정, 통계 검토 - 하수 관로 검색 로직 추가 2026-02-12 16:40:03 +09:00
rsone001 12bed29eb9 fix: any type수정중 2026-02-12 13:42:17 +09:00
rsone001 6817d34bc3 Merge remote-tracking branch 'origin/feature/review-skeleton-sein' into iipbs/st/dev 2026-02-12 11:20:26 +09:00
rsone001 efbef8283b feat: 02.하수관로 tab2 시설별 시설현황 컬럼 매칭 2026-02-12 11:10:46 +09:00
rsone001 af7c2a4f6f Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-02-12 10:50:13 +09:00
rsone001 60c515a654 st: fix & refactor: 하수도 보급률 입력 조회 response 수정 및 TadaGrid 컴포넌트 Tab state 부모 컴포넌트에서 prop로 전달하도록 수정, grid 데이터를 가져오기 위한 ref prop 수정(TabDataGrid.tsx는 아직 미구현) 2026-02-12 10:50:07 +09:00
chanii8 78e3e8e20e [st] design: 디자인 수정 2026-02-12 10:22:35 +09:00
dongsilove ac598c7215 wr: 하수재이용시설등록 버튼 2026-02-11 22:46:54 +09:00
dongsilove 321911371a wr: (map-layout) layout 수정 2026-02-11 22:07:00 +09:00
mind-one005 7e82714678 메뉴와 화면제목 일치 wr, fo 2026-02-11 18:17:39 +09:00
mind-one005 fa6f2fdcd7 fo: exceldown , 추가 -수질,계획하수량 2026-02-11 16:58:52 +09:00
chanii8 3b23113955 [st] chore: 검증 항목 관리 테이블 컬럼 수정 2026-02-11 15:45:51 +09:00
chanii8 fbe355f6cf [st] feat: DataGrid 컴포넌트에 행 클릭 이벤트 설정 2026-02-11 15:45:38 +09:00
chanii8 c439d3e58a [st] feat: 검증 항목 관리 레이아웃 작업 2026-02-11 15:42:01 +09:00
chanii8 057175b65c [st] rename: 파일명 변경 2026-02-11 15:41:49 +09:00
chanii8 794fdc482f Merge branch 'feature/review-skeleton-sein' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-02-11 15:40:06 +09:00
dongsilove 28d7a1bd29 Merge branch 'iipbs/sn/dev' 2026-02-10 23:08:10 +09:00
dongsilove 8d5546abd5 fo: 처리구역 exceldown 수정 2026-02-10 22:55:58 +09:00
dongsilove e264159323 fo: 수질현황 팝업 exceldown 수정 2026-02-10 22:41:56 +09:00
dongsilove 7650ec30e8 fo: dashboard 수정 2026-02-10 20:30:41 +09:00
mind-one005 c8912357b2 fo: exceldowntl , 추가 - 표로보기 2026-02-10 18:02:47 +09:00
rsone001 e79590b70d refactor: service 통합,인터페이스 네이밍 수정 2026-02-10 17:15:41 +09:00
rsone001 6904144ef8 refactor: 로딩기능 ,시도 선택, response 응답수정 2026-02-10 17:14:46 +09:00
rsone001 be2dcdc6f0 feat: 02.하수관로 tab1지역별 시설현황 조회 기능 추가 2026-02-10 17:08:05 +09:00
rsone001 636af7b14d refactor: 구분컬럼 변경rgnNm-> displayYear 2026-02-10 17:04:52 +09:00
mind-one005 a7d7ffb767 wr: chRowspanTable 수정 2026-02-10 13:49:48 +09:00
LGS 0de9e66988 대민서비스 물재이용 랜딩 페이지 (수요신청 페이지 추가) 2026-02-10 11:06:54 +09:00
mind-one005 e376fc7373 Merge branch 'iipbs/sn/dev' 2026-02-10 10:48:50 +09:00
rsone001 0f7e8c1de3 feat: 02 하수관로 tab1 지역별 시설현황 조회 기능 추가
footer 추가 구현 필요
2026-02-10 10:16:56 +09:00
mind-one005 c581698591 fo: 공법 추가 2026-02-10 10:13:32 +09:00
dongsilove aacf652dac fo: 처리시설 팝업 수정 2026-02-09 23:14:23 +09:00
dongsilove 3787f824df fo: exceldown시 , 추가 2026-02-09 22:38:10 +09:00
dongsilove 8d481aab09 fo: 처리시설 시설정보 2026-02-09 21:40:02 +09:00
mind-one005 21dd8f47a1 wr: 지도점검결과 등록 화면 운영관리코드명으로 보이도록 2026-02-09 18:18:11 +09:00
rsone001 0b14d1c0b5 Merge remote-tracking branch 'origin/iipbs/st/dev' into feature/review-skeleton-sein 2026-02-09 16:19:09 +09:00
rsone001 226c61eee9 st: fix: setFields 호출 직후 행 데이터 적용하도록 수정 2026-02-09 10:49:28 +09:00
rsone001 8d1af396ec Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-02-09 10:11:30 +09:00
rsone001 89df17aaeb st: fix: excel import & export 시 데이터 필드명을 맞추기위한 로직 수정 2026-02-09 10:11:23 +09:00
rsone001 e762720a01 feat: 00.통계입력현황 데이터 매핑 기능 및 api연동 2026-02-06 12:32:18 +09:00
chanii8 3b8fd2129b Merge branch 'iipbs/cs/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-02-06 11:27:40 +09:00
chanii8 8565331e68 [cs] feat: 다이얼로그 닫힐 때 폼 및 쿼리 로직 초기화 2026-02-06 11:26:40 +09:00
rsone001 d0faa58f3c Merge remote-tracking branch 'origin/iipbs/st/dev' into feature/review-skeleton-sein 2026-02-05 15:26:13 +09:00
rsone001 064898333c feat: 00.통계입력현황 컬럼매칭 수정중 2026-02-05 15:24:37 +09:00
chanii8 94f8e53184 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-02-05 15:15:14 +09:00
chanii8 d08df3ff35 [st] feat: 검증 이력 그리드 목데이터 작업 2026-02-05 15:14:54 +09:00
rsone001 4b0b08761c st: 검토 메뉴 grid footer 수정 및 누락 부분 추가 2026-02-05 14:47:47 +09:00
mind-one005 610d3d1c38 Merge branch 'iipbs/sn/dev' 2026-02-05 13:19:47 +09:00
rsone001 56d47ca1d0 Merge remote-tracking branch 'origin/feature/review-skeleton-sein' into iipbs/st/dev 2026-02-05 11:06:20 +09:00
rsone001 0e90277e90 st: fix: 입력-하수도보급률 컬럼 정보 수정 2026-02-05 11:04:25 +09:00
rsone001 18413509fc Merge remote-tracking branch 'origin/iipbs/st/dev' into feature/review-skeleton-sein 2026-02-05 10:50:15 +09:00
rsone001 cfddd6c0ae commit 2026-02-05 10:44:48 +09:00
rsone001 d861cfa811 feat: 01.하수도 보급률 컬럼 매칭 및 footer 수정중 2026-02-05 10:44:23 +09:00
chanii8 f88d42493c Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-02-05 10:02:39 +09:00
jcchoi 0459bcd4fd Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-02-04 19:42:57 +09:00
rsone001 eca74c1f2e st: refactor: 연도 & 전년도 데이터 가져오기 & 그리드 내 데이터 editable 판별 로직 수정 2026-02-04 17:54:02 +09:00
rsone001 42d6010efa st: refactor: 시도 및 검색연도 로직 수정
feat: 하수도보급률 전년도 데이터 가져오기 기능 추가 및 데이터 조회 시 입력 기한에 맞춰 그리드 셀 수정 or 수정 불가 로직 추가
2026-02-04 15:21:38 +09:00
mind-one005 206cd10844 sn: 하수관망도 팝업-기본정보 항목 추가 2026-02-04 13:50:45 +09:00
rsone001 ded46a7b6d Merge remote-tracking branch 'origin/iipbs/st/dev' into feature/review-skeleton-sein 2026-02-04 13:23:40 +09:00
chanii8 7227b98961 [cs] feat: 대시보드 페이지 일정 패널 작업 2026-02-04 09:34:50 +09:00
rsone001 6f07aca860 st: refactor: 시군구 검색 시 bsnmCd도 가져오도록 수정 2026-02-03 18:01:16 +09:00
chanii8 912e335b27 [cs] feat: 대시보드 페이지 실시간 알림 패널 작업 2026-02-03 17:48:15 +09:00
chanii8 b9cb8959ee [st] feat: 통계 검증 규칙 관리 화면 레이아웃 작업 2026-02-03 17:13:38 +09:00
rsone001 4ee12d127d st: refactor: cell 색상 속성 수정 2026-02-03 17:06:11 +09:00
rsone001 830cbf97d1 st: refactor: 구분 별 footer 평균 값 계산 함수 추가 2026-02-03 16:46:06 +09:00
rsone001 d32d73a681 Merge remote-tracking branch 'origin/iipbs/st/dev' into feature/review-skeleton-sein 2026-02-03 15:33:56 +09:00
rsone001 9eb40c46e5 st: fix: cell style 속성 추가, grid 데이터 변환 공통함수 추가 2026-02-03 15:29:45 +09:00
chanii8 8a160dc97c [cs] feat: 게시판 통계 페이지 데이터 바인딩 작업 2026-02-03 10:47:23 +09:00
mind-one005 ddb3693358 fo: 모래시계 수정 2026-02-02 18:14:45 +09:00
rsone001 1884f3733d Merge branch 'iipbs/st/dev' into feature/review-skeleton-sein 2026-02-02 17:33:59 +09:00
rsone001 15dd319b21 st: fix: 그리드 셀 편집 시 계산 로직 추가, 구분 별 footer 계산 로직 추가, footer 평균 계산 함수 추가, 각종 오류 수정 2026-02-02 16:49:10 +09:00
mind-one005 2947fb1524 Merge remote-tracking branch 'origin/iipbs/sn/dev' 2026-02-02 11:21:29 +09:00
mind-one005 2582bdd08c Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-02-02 11:19:59 +09:00
dongsilove 85615a826e fo: exceldown 로직 변경 2026-01-31 16:05:21 +09:00
dongsilove 2e24da3c83 Merge branch 'iipbs/sn/dev' 2026-01-30 21:38:13 +09:00
mind-one005 c6cfedb28b fo: 유량현황 팝업 강우시 강우일 음영표시 2026-01-30 18:14:30 +09:00
mind-one005 164fc1388e fo: 강우시 강우일의 강우량에 음영 2026-01-30 16:22:51 +09:00
mind-one005 39fb1d974a fo: excel 조회 결과 2026-01-30 10:39:41 +09:00
mind-one005 88eb32f4fc Merge branch 'iipbs/sn/dev' 2026-01-30 09:05:51 +09:00
dongsilove 1aaa8fcd6b fo: pagination 수정 2026-01-29 21:25:40 +09:00
dongsilove 527b0c6567 fo: exceldownload 데이터가 없는 경우 처리 2026-01-29 20:56:43 +09:00
mind-one005 a2504f4c85 fo:exceldown시 조건 해제 4개화면 2026-01-29 18:07:08 +09:00
rsone001 68c5548252 st: refactor:
- 컴포넌트 전체적으로 정리
- 그리드 컴포넌트 sort 기능 및 foooter 기능 수정
- menu active 기능 수정
2026-01-29 10:23:05 +09:00
mind-one005 ee9e309413 Merge branch 'iipbs/sn/dev' 2026-01-29 09:27:26 +09:00
mind-one005 5faced2928 fo: default 년도 설정 2026-01-29 09:12:00 +09:00
mind-one005 4356ec8a34 fo: 년도 초기화 및 exceldown시 조회데이터 없는 경우 처리 3본 2026-01-28 18:05:48 +09:00
mind-one005 a61fda8b58 fo: exceldown시 조건 나머지 적용 2026-01-26 16:13:33 +09:00
mind-one005 cf61a8956b fo: exceldown시 조건 2026-01-26 12:57:00 +09:00
dongsilove 9fc6be1256 Merge branch 'iipbs/sn/dev' 2026-01-22 22:03:33 +09:00
dongsilove 30b98de0a1 fo: planSewage 수정 2026-01-22 21:57:42 +09:00
dongsilove da1107566c fo: 수질현황, 계획하수량 2026-01-22 21:47:50 +09:00
mind-one005 974d15962c fo: exceldownload 4본 수정 2026-01-22 11:26:43 +09:00
dongsilove bd5c2f07d3 fo: 오수발생량 수정중 2026-01-21 22:15:02 +09:00
jcchoi 2469a84068 대시보드 게시글 링크 수정정 2026-01-21 17:52:22 +09:00
jcchoi 80598a5b6a Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-01-21 16:28:08 +09:00
mind-one005 b4a905d2ed Merge branch 'iipbs/sn/dev' 2026-01-21 10:35:28 +09:00
jcchoi 599e8a95c7 logout 수정정 2026-01-20 15:00:08 +09:00
mind-one005 3eb8861d06 dm: 물재이용현황 시도 수정 2026-01-19 18:11:56 +09:00
mind-one005 122e10f851 wr: 재원협의 문구 2026-01-19 16:38:26 +09:00
mind-one005 e464d298ad wr: 36 계통도 평면도 외 2026-01-19 13:24:29 +09:00
dongsilove 44580f5156 fo:수질현황 총인설비tab 2026-01-18 21:21:19 +09:00
dongsilove 69d73ae3fb Merge branch 'main' into iipbs/sn/dev 2026-01-16 20:16:24 +09:00
mind-one005 943d858c5a cm: 우편번호 떼어내기 2026-01-16 17:37:49 +09:00
rsone001 9881653bae fix: 12. 하수 밒 분뇨 관련 사업 컬럼매핑오류(한글화) 수정 2026-01-16 13:22:34 +09:00
mind-one005 2fe73c7480 Merge branch 'iipbs/sn/dev' 2026-01-16 13:08:27 +09:00
rsone001 af636453e0 Merge remote-tracking branch 'origin/feature/review-skeleton-sein' into iipbs/st/dev 2026-01-16 13:05:58 +09:00
rsone001 51d8eb50aa feat : 01. 하수도 보급률
valueType,numberFormat 추가,컬럼명 변경
2026-01-16 11:54:49 +09:00
rsone001 e94782f56a st: feat: 멀티 footer, footer 병합 작업중 2026-01-16 11:54:36 +09:00
rsone001 8552957fb5 feat : 13. 하수도 서비스 수준
valueType,numberFormat 추가,컬럼명 변경
2026-01-16 11:52:23 +09:00
rsone001 35a44ff7aa feat : 10. 물 재이용
valueType,numberFormat 추가,컬럼명 변경
2026-01-16 11:48:58 +09:00
rsone001 93d090b23f feat : 00. 통계 입력 현황
valueType,numberFormat 추가,컬럼명 변경
2026-01-16 11:48:24 +09:00
rsone001 66930f2e93 feat : 09. 하수도 재정
valueType,numberFormat 추가,컬럼명 변경
2026-01-16 11:47:13 +09:00
rsone001 0240e1fc7a feat : 04. 하수저류 시설 및 빗물 펌프장
valueType,numberFormat 추가,컬럼명 변경
2026-01-16 11:46:55 +09:00
rsone001 011104b548 feat : 02. 하수관로
valueType,numberFormat 추가,컬럼명 변경
2026-01-16 11:46:37 +09:00
rsone001 c077c0507c feat : 03. 하수관로 유지 관리
valueType,numberFormat 추가,컬럼명 변경
2026-01-16 11:46:04 +09:00
rsone001 042f839e41 feat : 11. 하수 및 분뇨 찌꺼기(슬러지) 처리
valueType,numberFormat 추가,컬럼명 변경
2026-01-16 11:44:58 +09:00
rsone001 292c17d682 feat : 12. 하수 및 분뇨 관련 산업valueType,numberFormat 추가,컬럼명 변경 2026-01-16 11:44:24 +09:00
rsone001 e875700105 feat : 05.펌프장valueType,numberFormat 추가,컬럼명 변경 2026-01-16 11:43:51 +09:00
rsone001 c036ce41c9 feat : 06.공공하수처리시설valueType,numberFormat 추가,컬럼명 변경 2026-01-16 11:43:07 +09:00
rsone001 b4f3c0108e feat : 08.개인하수처리시설valueType,numberFormat 추가,컬럼명 변경 2026-01-16 11:42:10 +09:00
rsone001 2a6fa899e3 feat : 07.분뇨처리시섪valueType,numberFormat 추가,컬럼명 변경 2026-01-16 11:41:27 +09:00
dongsilove 3716e8c7b2 dm: 로그아웃처리 2026-01-15 21:14:49 +09:00
mind-one005 4e27466f00 dm: GATEWAY_URL 추가 2026-01-15 15:51:08 +09:00
mind-one005 21b043cbae dm: 유사사례 사업명 ***처리 2026-01-14 17:51:48 +09:00
mind-one005 7588ee4e6a dm: 유사사례 팝업 조정 2026-01-14 17:05:26 +09:00
dongsilove c57f05cc3e sn: showModal 수정 2026-01-13 22:53:35 +09:00
rsone001 8252561050 st: refactor: grid 변경 내용 @rsone/components 모듈에 반영, 모든 컴포넌트에서 @rsone/components의 grid 컴포넌트를 import 하도록 수정, 검토 관련 컴포넌트 코드 정렬 및 불필요 공백 제거 2026-01-13 15:58:19 +09:00
rsone001 20cbd7ebcd Merge remote-tracking branch 'origin/feature/management-skeleton-eunsun' into iipbs/st/dev 2026-01-13 11:46:57 +09:00
rsone001 71e412d030 st: feat:
- compact grid(1/2 넓이) & split grid(1/2 넓이 2개) 컴포넌트 추가
- 시설별 설치 현황 관리 버튼 추가
- 통계입력현황(검토) 컴포넌트 & 컬럼 파일 오타 수정
2026-01-13 11:46:28 +09:00
rsone001 bb1cbb9ac0 refactor: 각 tab의 컬럼 파일명 단축 및 오타 수정
- 가독성 개선을 위해 파일명 단축
- Treatment -> Treat
- Facility -> Fac
- Construction -> Const
- Approval -> Appr
- Utilization -> Util
- Designation -> Desig
- Settlement -> Settle
- StatsInputSatus 오타 변경 -> StatsInputStatus
2026-01-13 11:43:20 +09:00
rsone001 7af1ef94cb feat: 하수도 통계 입력에 '해당사항 없음' 버튼 추가
- 04. 하수저류시설 및 빗물펌프장
- 05. 펌프장
- 06. 공공하수처리시설 (운영 데이터 업데이트 버튼도 추가)
- 07. 분뇨처리시설
- 10. 물 재이용
- 11. 하수 및 분뇨찌꺼기(슬러지)처리
- 13. 하수도 서비스 수준
- 추후 조건에 따라서 버튼 보여지고 안보여지고 제어예정
2026-01-13 10:47:36 +09:00
dongsilove 8f26c1482e fo: 총인설비 수질현황 기간별 exceldownload 2026-01-12 22:40:58 +09:00
rsone001 a3797c7ae1 refactor: 하수도 통계 입력 > 10 ~ 13 컬럼 속성에 valueType, dataForma 추가
- 숫자면 valueType, dataFormat 추가
- 버튼 정렬 맞춤 <div> 추가
- 항목 추가, 항목 제외 버튼 추가 : 10, 11, 12
- 05. 펌프장 컬럼명 중복 해결
- 10. 물 재이용, 11. 하수 및 분뇨찌꺼기(슬러지)처리, 12. 하수 및 분뇨관련산업, 13. 하수도 서비스 수준
2026-01-12 17:56:58 +09:00
rsone001 2cfcddc365 st: refactor: 해당사항 없음 버튼 css 수정 2026-01-12 17:01:07 +09:00
rsone001 7fa390ccb1 Merge remote-tracking branch 'origin/feature/management-skeleton-eunsun' into iipbs/st/dev 2026-01-12 16:00:14 +09:00
rsone001 5e7eae3e9e 하수도 통계 관리 > 05 ~ 09 컬럼 속성에 valueType, dataFormat 추가
- 숫자면 valueType 추가
- 입력값과 합계값의 소수점 불일치는 입력값 기준으로 수정
- 버튼 정렬 맞춤 <div> 추가
- 항목 추가, 항목 제외 버튼 추가 : 05, 07
- 운영데이터 가져오기 버튼 아래로 이동 : 06
- 05. 펌프장, 06. 공공하수처리시설, 07. 분뇨처리시설, 08. 개인하수처리시설, 09. 하수도재정 추가 완료
2026-01-12 15:54:08 +09:00
rsone001 b83a55d1af Merge remote-tracking branch 'origin/feature/management-skeleton-eunsun' into iipbs/st/dev 2026-01-12 15:27:15 +09:00
rsone001 2eddbacc7f st: feat: 신규 버튼 컴포넌트 추가, 50:50 비율 그리드 추가 작업중 2026-01-12 15:26:33 +09:00
dongsilove fe216499ec fo: 주석 2026-01-12 13:43:26 +09:00
rsone001 1b64bccc55 refactor: 하수도 통계 관리 > 00 ~ 04 컬럼 속성에 valueType, dataFormat 추가
- 숫자면 valueType 전부 추가
- 입력값과 합계값의 소수점 불일치는 입력값 기준으로 수정
- 00. 통계 입력 현황 - 작성율만 숫자
- 01. 하수도 보급율, 02. 하수관로, 03. 하수관로 유지관리, 04. 하수저류시설 및 빗물펌프 추가 완료
2026-01-12 13:26:43 +09:00
dongsilove 36294fef80 fo: 처리구역 2026-01-12 10:28:36 +09:00
dongsilove 1eb4435c2f Merge branch 'iipbs/sn/dev' 2026-01-12 09:46:55 +09:00
dongsilove 94fa8eb721 fo: 유량현황 강우시 평균 단수 처리 2026-01-10 20:43:46 +09:00
dongsilove ac8c58d2a9 fo: dashboard 강우일운영현황 2026-01-10 15:33:36 +09:00
mind-one005 54c958e8fa fo: excel 컬럼 순서 fix 2026-01-09 18:10:19 +09:00
rsone001 57151eae76 Merge remote-tracking branch 'origin/feature/management-skeleton-eunsun' into iipbs/st/dev 2026-01-09 17:25:55 +09:00
rsone001 9729b42837 st: refactor: - 버튼 컴포넌트 추가 2026-01-09 17:25:16 +09:00
rsone001 fd106f3729 refactor: 컬럼 정의 파일명 변경 Mng -> Inp
- management -> input으로 변경됨에 따라 MngColumn.ts -> InpColumn.ts 로 모두 변경
2026-01-09 17:20:53 +09:00
rsone001 4da4f5cd62 feat: 13. 하수도서비스 수준 화면 구현
- 각 tab별로 columns, columnLayout 분리
- tab 1. 민원 (ComplaintInpColumn.ts)
- tab 2. 주민친화시설 (ResidentFriendlyFacilityInpColumn.ts)
- 하수도서비스 수준 Component 완성
2026-01-09 16:38:38 +09:00
mind-one005 58c8f118d3 fo: dashboard 강우일운영현황 2026-01-09 16:34:52 +09:00
mind-one005 a6514cab64 fo: dashboard 강우일 운영현황 tab 2026-01-09 16:26:10 +09:00
rsone001 c73a01d27d feat: 12. 하수 및 분뇨관련 산업 화면 구현
- 각 tab별로 column, columsLayout 분리
- tab 1. 공공기관 하수분야 (PublicInstitutionSewerIndustryInpColumn.ts)
- tab 2. 공공기관 분뇨분야 (PublicInstitutionExcretaIndustryInpColumn.ts)
- tab 3. 분뇨수집 · 운반업 (ExcretaRelatedIndustryInpColumn.ts)
- tab 4. 개인하수처리관련업 (PrivateSewageTreatmentRelatedIndustryInpColumn.ts)
- 하수 및 분뇨관련산업 컴포넌트 완성
2026-01-09 15:51:15 +09:00
rsone001 eae2c15aaf feat: 11. 하수 및 분뇨찌꺼기(슬러지)처리 화면 구현
- tab 1. 찌꺼기(슬러지)처리 시설현황 columns, columnLayout 완성 (SlurgeProcessingFacStatusInpColumn.ts)
- tab 1 컬럼에서 DB 테이블과 mapping되지 않는 컬렴들은 임의로 생성
- tab 2. 소화조 운영현황 columns, columnLayout 완성 (DigestorOperatingStatusInpColumn.ts)
- tab 3. 하수찌꺼기 처리시설 운영현황 columns, columnLayout 완성 (SewageSlurgeProcessingFacStatusInpColumn.ts)
- tab 4. 분뇨찌꺼기(슬러지) 처리현황 columns, columnLayout 완성 (ExcretaSlurgeProcessingStatusInpColumn.ts)
- 하수 및 분뇨찌꺼기(슬러지)처리 컴포넌트 완성
2026-01-09 14:42:03 +09:00
mind-one005 f279f49865 fo: build 오류 수정 2026-01-09 13:38:39 +09:00
rsone001 ece49f0ae2 st: refactor: 경로 오류 수정 2026-01-09 13:30:28 +09:00
rsone001 810bedb414 Merge branch 'iipbs/st/dev' of https://onasys.co.kr:3000/ONAANDALL/frontend into feature/management-skeleton-eunsun 2026-01-09 13:26:05 +09:00
rsone001 5492ea70ed st: refactor:
- 통계 입력 디렉토리 & 파일명 변경
management -> input & Mng -> Inp
- @rsone/component 모듈 수정
pnpm build --filter @rsone/components 후 st 프로젝트 install 필요
2026-01-09 13:21:55 +09:00
mind-one005 39de22045b fo: build 2026-01-09 13:21:38 +09:00
rsone001 f61e952d28 feat: 11. 하수 및 분뇨찌꺼기(슬러지) 화면 구현 중
- 각 tab을 위한 columns, columnLayout 파일 생성
- tab 1. 찌꺼기(슬러지)처리 시설현황 구현 중
2026-01-09 13:20:55 +09:00
mind-one005 b960a03361 fo: 사용하지 않는 변수 정리 2026-01-09 13:19:34 +09:00
mind-one005 e5a750e8b9 fo: eslint 수정 2026-01-09 13:06:41 +09:00
rsone001 b5709ab2e2 Merge remote-tracking branch 'origin/feature/management-skeleton-eunsun' into iipbs/st/dev 2026-01-09 10:21:42 +09:00
rsone001 d9d5b49057 st: refactor:
- Real Grid 컴포넌트 @rsone/components 로 모듈화
- 컴포넌트 동적 import 경로 수정(engName -> urlPath)
2026-01-09 10:21:00 +09:00
dongsilove 6a5c7c301f fo: dashboard 지하수유입량 수정 2026-01-08 22:24:32 +09:00
mind-one005 295247845c fo: 지하수유입량 최대, 최소 수정 2026-01-08 17:58:38 +09:00
rsone001 1c7be567f4 feat: 10. 물 재이용 화면 구현
- tab 2. 중수도 현황 columns, columnLayout 완성 (RecycledWaterStatusMngColumn.ts)
- tab 3. 빗물이용시설현황 columns, columnLayout 완성 (RainfallUtilizationFacilityStatusMngColumn.ts)
- tab 4. 하·폐수처리수 재이용시설 설계시공업 등록현황 columns, columnLayout 완성 (TreatmentWaterReuseDesignConstructionStatusMngColumn.ts)
- tab 5. 하·폐수처리수 재이용사업 인가현황 columns, columnLayout 완성 (TreatmentWaterReuseBussinessApprovalStatusMngColumn.ts)
- 물 재이용 컴포넌트 완성 tab list 추가
2026-01-08 17:11:14 +09:00
rsone001 c4ca48be62 refactor: 네이밍, footer, search form 수정
- 구군 -> 시군구 로 변경
- footer 합계 추가 (05. 펌프장, 06. 공공하수처리시설, 07. 분뇨처리시설)
- search form 시설명 누락 추가 (04. 하수저류시설 및 빗물펌프장, 05. 펌프장, 06. 공공하수처리시설, 07. 분뇨처리시설)
2026-01-08 15:38:07 +09:00
mind-one005 5b76d02e0b fo: 강수현황 excel문제 해결 2026-01-08 13:57:29 +09:00
dongsilove 9b1342b203 fo: 처리구역정보 2026-01-07 21:46:16 +09:00
rsone001 a4147a4c67 Merge remote-tracking branch 'origin/feature/management-screen-eunsun' into iipbs/st/dev 2026-01-07 11:32:30 +09:00
rsone001 0b8ace2f0d feat: 10. 물 재이용 화면 구현 중
- 각 tab을 위한 columns,comlumLayout 파일 생성
- tab 1. 공공하수처리 재이용 정의 완성 (PublicSewageTreatmentWaterReuseMngColumn.ts)
2026-01-07 11:28:36 +09:00
rsone001 185bda696a Merge remote-tracking branch 'origin/feature/management-screen-eunsun' into iipbs/st/dev 2026-01-07 11:17:04 +09:00
rsone001 2e03b79031 Merge remote-tracking branch 'origin/yangsein' into iipbs/st/dev 2026-01-07 11:16:00 +09:00
dongsilove b57858c5ef fo: 총인시설여부 수정 2026-01-07 11:15:43 +09:00
rsone001 cbded3e605 st: refactor: 그리드 prop isExcelExport(엑셀 내보내기 버튼 유무)추가, mergeRule, sortdisable 추가
컬럼 속성에 mergeRule: true시 해당 컬럼 같은 값인 cell merge
sortdisable: true시 해당 컬럼의 정렬 기능 비활성화
2026-01-07 11:07:56 +09:00
mind-one005 c01de13bae Merge branch 'iipbs/sn/dev' 2026-01-06 16:12:23 +09:00
mind-one005 a0ad12af49 fo: api /dashboard/mngInfo 2026-01-06 16:08:36 +09:00
rsone001 81306a7b82 refactor: 09. 하수도 재정 tab 구조 분리 & footer 적용
- tab별 컬럼 정의 파일 분리 (SewerageFinanceSettlementMngColumn, SewerageChargeMngColumn, SewerageUsageFeeMngColumn, CauserBurdenMngColumn)
- 각각의 컬럼 정의 파일에 footer 설정 적용 (정수, 소수점 1자리)
- editable 속성 추가
- tab 3: 하수도 사용료 컬럼 구조 수정
2026-01-06 15:25:13 +09:00
rsone001 c6d66c54b5 refactor: 08. 개인하수처리시설 tab 구조 분리 & footer 적용
- tab별 컬럼 정의 파일 분리 (SewageTreatmentFacilityStatusMngColumn, SepticTankStatusMngColumn, PrivateSewageDesignationStatusMngColumn)
- 각각의 컬럼 정의 파일에 footer 설정 적용 (정수, 소수점 1자리)
- editable 속성 추가
2026-01-06 11:13:20 +09:00
rsone001 69a0dd47ff [fix] 00. 통계입력현황 탭grid->단일 grid로 변경 2026-01-06 11:01:09 +09:00
rsone001 1ad61e0fa5 [fix]05.펌프장 탭grid->단일 grid로 변경 2026-01-06 11:00:41 +09:00
rsone001 4b895ac5d4 [fix]06.공공하수처리시설 탭grid->단일 grid로 변경 2026-01-06 11:00:09 +09:00
rsone001 f86301a771 [fix]07.분뇨처리시설 탭grid->단일 grid로 변경 2026-01-06 10:59:21 +09:00
jcchoi 7a231c541a Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-01-06 09:46:31 +09:00
jcchoi 2a66772bfa 쿠키 삭제 도메인 처리 2026-01-06 09:46:27 +09:00
rsone001 f7c691ef9f refactor: 화면정의서 작성용 코드 추가
- 화면 정의서 화면용 버튼 추가
2026-01-06 00:12:29 +09:00
rsone001 2bf04dd288 refactor: 07. 분뇨처리시설 컬럼 파일로 분리 & footer 적용
- ExcretaTreatmentFacilityMngColumn.ts 파일 생성
- 기존 ExcretaTreatmentFacilityMng.tsx 컴포넌트 내 컬럼 정의 별도 파일로 분리
-  footer 설정 적용 (소수점 3자리, 소수점 1자리, 정수)
- editable 속성 추가
2026-01-05 17:19:57 +09:00
rsone001 96aecb9847 Merge remote-tracking branch 'origin/feature/management-screen-eunsun' into iipbs/st/dev 2026-01-05 17:17:11 +09:00
rsone001 e0b631ec25 st: refactor: button style 수정 2026-01-05 17:09:42 +09:00
rsone001 55bf8df00d refactor: 06. 공공하수처리시설 컬럼 파일로 분리 & footer 적용
- PublicSewageFacilityMngColumn.ts 파일 새로 생성
- 기존 PublicSewageFacilityMng.tsx 내 컬럼 정의 별도 파일로 분리
- footer 설정 적용 (소수점 3자리, 소수점 1자리, 정수)
- editable 속성 추가 (수정 가능 컬럼 10개)
2026-01-05 16:02:11 +09:00
mind-one005 69c068e823 fo: dashboard 주석2 2026-01-05 15:55:45 +09:00
rsone001 591f47a3ff refactor: 00. 통계입력현황 컬럼 파일로 분리
- StatsInputSatusMngColumn.ts 파일 생성 후 컬럼 정보 정의
- StatsInputSatusMng.tsx 컴포넌트 수정 (컬럼 정보 파일 import)
2026-01-05 15:07:00 +09:00
rsone001 c281f688b5 refactor: 컬럼 정의 네이밍 컨벤션 적용 (-MngColumn)
- 통계입력관리 관련 컬럼 정의 파일 네이밍 통일 '-MngColumn'형식으로 표준화
- export 변수명을 파일명과 일치하도록 수정
- 참조하는 파일들의 import 경로 수정
2026-01-05 14:44:48 +09:00
rsone001 97e448c5c8 refactor: 05. 펌프장 컬럼 파일로 분리 & footer 적용
-  PumpingStationMngColumn.ts 파일 새로 생성
- 기존 PumpingStation.tsx 내 컬럼 정의 별도 파일로 분리
- footer 설정 적용 (소수점 3자리, 소수점 1자리, 정수)
- editable 속성 추가 (시도, 시군구, 우수량, 총양수량 = false)
2026-01-05 13:18:45 +09:00
rsone001 8de5317d28 st: refactor: 그리드 merge rule 로직 변경 / 컬럼 sort 여부 추가 2026-01-05 10:04:20 +09:00
dongsilove 955c969fc5 fo:주석 추가 2026-01-05 06:50:18 +09:00
dongsilove ac3acbb0f4 fo: dashboard 수정 2026-01-04 20:45:08 +09:00
dongsilove 897ce3eb24 wr: 주석 추가 2026-01-03 15:52:41 +09:00
dongsilove 94b640d566 dm: experimental 추가 2026-01-03 14:50:26 +09:00
dongsilove bd8d850a78 dm: 연녹색 스킨 및 메뉴 2026-01-02 23:17:59 +09:00
mind-one005 43af9042c8 fix fo 2026-01-02 18:21:12 +09:00
mind-one005 e283c91394 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-01-02 18:04:23 +09:00
mind-one005 63543a016e fo: dashboard 물사용량 수정 2026-01-02 18:04:19 +09:00
swcho f96ae66385 fix: 개발 서버 에러 수정 테스트 롤백 2026-01-02 17:49:36 +09:00
swcho 8f531a245f fix: 개발서버 env 파일 확인 2026-01-02 17:41:52 +09:00
swcho ea738d687b fix: router.push -> window.location.href로 변경 2026-01-02 16:11:00 +09:00
swcho 8cc5a0a737 fix: base_url 수정 2026-01-02 15:58:35 +09:00
swcho d4b32ffad0 fix: 서브도메인 포트 수정 2026-01-02 15:39:28 +09:00
swcho 911e97791d Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-01-02 15:18:08 +09:00
swcho e0e1bb5b1a fix: 개발 서버 에러 수정 테스트2 2026-01-02 15:18:06 +09:00
mind-one005 61aa2ff1e1 Merge branch 'main' into iipbs/sn/dev 2026-01-02 15:00:36 +09:00
mind-one005 cdf3dfd18f Merge branch 'iipbs/sn/dev' 2026-01-02 14:55:18 +09:00
rsone001 54e60d83a0 [refactor] 불필요한 주석 제거 2026-01-02 14:29:58 +09:00
rsone001 25e41a356b [feat]01.통계입력현황 단일탭 column 폴더 분리 2026-01-02 14:10:33 +09:00
swcho 745a7d06ea fix: 개발 서버 에러 수정 테스트 2026-01-02 13:55:02 +09:00
rsone001 7abae34531 [feat] 01.하수도 보급율 단일탭 column 폴더분리 2026-01-02 13:53:01 +09:00
rsone001 877a548c6f [feat]05. 펌프장 단일 탭 column폴더 분리 2026-01-02 13:41:10 +09:00
rsone001 28f841adc8 [feat] 06.공공 하수처리시설 단일 탭 column 분리 2026-01-02 13:10:16 +09:00
rsone001 69820d8e4c [feat] 07.분뇨처리시설 단일 탭 column 폴더로 분리 2026-01-02 11:26:27 +09:00
rsone001 36146c3fc4 [feat] map 처리로 columns editable 옵션 추가 및 코드 정리 2026-01-02 11:18:36 +09:00
dongsilove 7af6f3a324 UiTable 수정 2026-01-01 22:13:04 +09:00
dongsilove 36deb89f25 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2025-12-31 18:06:20 +09:00
dongsilove 2364de0616 NEXT_PUBLIC_NGII_URL 수정 2025-12-31 18:06:16 +09:00
rsone001 8318888f6d refactor: 04.하수저류시설 및 빗물펌프 tab 구조 분리 & footer 적용
- tab별 컬럼 정의 파일 분리 (SewageReservoirColumn, RainfallPumpStationColumn)
- 저장 로직에서 필요한 컬럼은 hidden 컬럼으로 변경, footer 적용
2025-12-31 14:57:51 +09:00
rsone001 7e9f294ae8 merge origin/feature/management-screen-eunsun into iipbs/st/dev 2025-12-31 13:29:44 +09:00
rsone001 7d51674516 refactor: 03. 하수관로 유지관리 tab 구조 분리 & footer 적용
- 기존 tab 구조를 RepairStatus(개보수), PipeDredgingStatus(관로준설) 파일로 분리
- 각 파일에 footer 속성, editable 속성 추가
2025-12-31 13:21:59 +09:00
rsone001 3830273119 Merge remote-tracking branch 'origin/feature/management-screen-eunsun' into iipbs/st/dev 2025-12-31 13:18:03 +09:00
rsone001 d065fe669b st: refactor:
- data state 선언 위치 변경
- 데이터 필드 타입 및 format 설정 로직 추가
2025-12-31 13:17:32 +09:00
rsone001 9fcd719382 refactor: 02. 하수관로 화면 구조 수정 & footer 적용
- tab 별 grid 컬럼 정의 파일 분리 (지역별 시설현황, 시설별 설치현황, 관종별 현황, 관경별 현황, 맨홀 현황, 시설별현황관리)
- tab 5. 맨홀 현황 새로 생성
- 각 tab에 footer 속성 추가
2025-12-31 11:27:34 +09:00
jcchoi 6ff2e5b0a2 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-12-31 10:24:01 +09:00
jcchoi 67d8ff5523 불필요한 파일 제거 2025-12-31 10:20:49 +09:00
rsone001 55b3752f49 Merge remote-tracking branch 'origin/feature/management-screen-eunsun' into iipbs/st/dev
refactor: DataGrid 컴포넌트를 사용하는 부모 컴포넌트에서 rowData state를 직접 생성 후 자식 컴포넌트로 내려주는 방식으로 수정, TabDataGrid는 동일 방식으로 수정할 예정
2025-12-30 17:59:06 +09:00
rsone001 32ad548de2 st: refactor: DataGrid 설정 수정 중 2025-12-30 17:55:05 +09:00
rsone001 074d82a3bd refactor: 02. 하수관로 tab 구조 개선 & grid footer 설정 수정
- 개별 컬럼 정의 파일들을 리스트로 묶어 TabDataGrid에 직접 전달하는 방식으로 변경
- footerSummary.ts에 포맷별(정수/소수점) 설정 객체 추가 및 export
- SeweragePenetrationRateMngColumn.ts 파일에 footer 적용
2025-12-30 17:41:17 +09:00
mind-one005 8bedd8629a Merge branch 'main' into iipbs/sn/dev 2025-12-30 17:23:27 +09:00
mind-one005 a2cd08cbd9 feature 적용 위한 수정 2025-12-30 17:04:23 +09:00
rsone001 90c977300c refactor: 01. 하수도 보급률 컬럼 분리, Grid 속성 적용
- SeweragePenetrationRateMng.tsx 에 포함되어있던 컬럼 정의를 SeweragePenetrationRateMngColumn.ts 파일로 분리
- Footer 합계 함수 적용 (소수점, 정수 적용)
- 컬럼별 수정가능 여부(editable) 설정
2025-12-30 14:06:29 +09:00
rsone001 01ef993601 feat: 하수도 통계 관리 화면 구현
- 09. 하수도 재정 화면 구현
2025-12-30 11:25:42 +09:00
jcchoi 68669b2574 Update apps/cm/.env.example
샘플 수정
2025-12-30 11:23:10 +09:00
jcchoi b09e5c68f9 Delete apps/cm/.env.zip
환경설정 삭제
2025-12-30 11:21:50 +09:00
jcchoi a5d2a261ab Delete apps/cm/.env.production
환경설정 삭제
2025-12-30 11:21:42 +09:00
jcchoi a161cb2346 Delete apps/cm/.env.local
환경설정 삭제
2025-12-30 11:21:34 +09:00
jcchoi a6b03ddc4e Delete apps/cm/.env.development
환경설정 삭제
2025-12-30 11:21:21 +09:00
mind-one005 ae96324783 fo: undefined 수정 2025-12-30 10:08:36 +09:00
mind-one005 4cb1c47d61 fo: dashboard 속도개선 2025-12-30 09:51:32 +09:00
mind-one005 33bf93be8c Merge remote-tracking branch 'origin' into iipbs/sn/dev 2025-12-30 09:45:32 +09:00
dongsilove dafb3b0f9f fo: 수정중 2025-12-30 07:23:59 +09:00
jcchoi cb0cda5fe6 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-12-29 18:16:42 +09:00
jcchoi 6d0c8c9fba 환경 수정 2025-12-29 18:16:37 +09:00
dongsilove 39d0f586af fo: dashboard 수질 수정 2025-12-29 17:22:06 +09:00
rsone001 a542cdd87d st: refactor: 헤더 컬럼 폴더 구조 정리 2025-12-29 16:23:50 +09:00
rsone001 f57fd44db9 Merge remote-tracking branch 'origin/eunsun' into iipbs/st/dev 2025-12-29 16:11:02 +09:00
rsone001 ce44289ff1 feat: 하수 통계 관리 화면 구현2
- 07. 분뇨처리시설
- 08. 개인하수처리시설
- 09. 하수도 재정 (구현 중 tab1까지 완료)
2025-12-29 15:58:25 +09:00
rsone001 52e94bc943 fix: 컬럼 로직 변경 전 commit 2025-12-29 15:03:22 +09:00
rsone001 7876fea738 Merge remote-tracking branch 'origin/eunsun' into iipbs/st/dev 2025-12-29 15:02:33 +09:00
rsone001 ef2e0ccfea Merge remote-tracking branch 'origin/yangsein' into iipbs/st/dev 2025-12-29 15:02:04 +09:00
rsone001 086478b41e fix: 메뉴별 컬럼 1차 셋팅 2025-12-29 14:58:47 +09:00
rsone001 66560d661e [feat] 물재이용 탭4개 추가 2025-12-29 14:14:32 +09:00
rsone001 c6f13b35a1 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2025-12-29 11:49:14 +09:00
rsone001 2009c54a00 fix: footer(합계) 작업 중 2025-12-29 11:49:07 +09:00
rsone001 822081b318 Merge branch 'main' into eunsun 2025-12-29 11:13:45 +09:00
yangsein 2080927302 [feat]물재이용 탭 1개 추가 2025-12-29 11:08:55 +09:00
dongsilove 2ec793fd59 fo: UiTable 수정 2025-12-29 11:00:38 +09:00
rsone001 f0272e1f82 feat: 하수 통계 관리 화면 구현
- 02. 하수관로
- 03. 하수관로 유지관리
- 04. 하수저류시설
- 05. 펌프장
- 06. 공공하수처리시설
2025-12-29 10:59:43 +09:00
yangsein b59ab18fa6 [feat] 하수 및 분뇨 찌꺼기(슬러지) 처리 tab4개 추가 2025-12-29 10:52:43 +09:00
dongsilove 9b5025703d fo: 계획하수량 산정지표 2025-12-27 22:50:10 +09:00
dongsilove 220d430dbe sn: 합류식,분류식 해결 2025-12-26 22:13:12 +09:00
mind-one005 b786a05e05 sn: 중점우수토실 구분 2025-12-26 18:06:58 +09:00
swcho d289e0776c Merge remote-tracking branch 'origin/main' into iipbs/sn/dev 2025-12-26 15:46:28 +09:00
mind-one005 24b11cb23f sn: row data -> raw data 2025-12-26 15:42:59 +09:00
swcho e37e7de6de fix: fo 로그아웃 로직 수정 2025-12-26 15:25:01 +09:00
swcho 0d1ac48bc6 fix: fo AuthWrapper 주석 해제 2025-12-26 14:27:30 +09:00
yangsein 8006a9ef09 [feat]하수및분뇨관리사업 탭 4개 추가 2025-12-26 13:52:43 +09:00
swcho d1b9c34634 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2025-12-26 13:48:22 +09:00
swcho 09158b6efe fix: 로그인 페이지 경로 수정 2025-12-26 13:48:21 +09:00
yangsein 97435b7113 [feat]하수도 서비스 수준 탭2개 추가 2025-12-26 13:39:44 +09:00
yangsein 6cee09b72e [feat]하수도 재정 탭 4개 추가 2025-12-26 13:28:14 +09:00
mind-one005 8cd394d946 fo: api 변경 2025-12-26 13:06:55 +09:00
yangsein 35ae83034c [feat]개인 하수처리시설 탭 3개 추가 2025-12-26 12:03:11 +09:00
yangsein deaf1b53ee [feat]하수 저류시설 및 빗물 펌프장 2025-12-26 11:41:36 +09:00
yangsein cda8044183 [feat]하수관리 유지관리 탭 2개 추가 2025-12-26 11:17:27 +09:00
mind-one005 275d840a5f fo: undefined type 정리 2025-12-26 11:05:45 +09:00
mind-one005 27d5e6bbd3 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2025-12-26 10:52:49 +09:00
mind-one005 493402dc2f fo:never used 변수 정리 외 2025-12-26 10:52:42 +09:00
swcho be5ccc779a fix: 상류관로 보기 에러 수정 (데이터 응답 구조 수정) 2025-12-26 10:49:16 +09:00
yangsein 47f41faee3 [feat]02.하수관로 탭 4개 추가 2025-12-26 10:47:39 +09:00
mind-one005 c195621128 Merge remote-tracking branch 'origin/main' into iipbs/sn/dev 2025-12-26 09:20:57 +09:00
dongsilove cd7584d5b0 fo: 계획하수량 산정지표 현황 수정 2025-12-25 22:00:15 +09:00
jcchoi 6d2518f50d Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-12-24 20:30:15 +09:00
jcchoi 54cd5235fd API 경로 수정정 2025-12-24 20:29:43 +09:00
mind-one005 2d8e29cea9 Merge branch 'main' into iipbs/sn/dev 2025-12-24 17:54:17 +09:00
mind-one005 494e6f076b sn: 중점우수토실 외 2025-12-24 17:05:08 +09:00
dongsilove 11b5573e66 sn:SUB_DOMAIN 추가 2025-12-23 21:54:08 +09:00
yangsein 3d7fc84a2e [feat]분뇨처리시설 화면 추가 2025-12-23 17:51:24 +09:00
yangsein 68f8a7be38 [feat]하수관로 화면 컬럼 추가 2025-12-23 15:07:43 +09:00
yangsein 4d537a7bbc Merge branch 'iipbs/st/dev' of https://onasys.co.kr:3000/ONAANDALL/frontend into yangsein 2025-12-23 14:13:50 +09:00
yangsein 6c6aaebdc8 [feat]통계 입력현황,하수도 보급율,펌프장,공공하수처리시설 2025-12-23 14:09:14 +09:00
rsone001 ba4062f826 feat: 그리드 텝 형식 컴포넌트 신규 생성
TabDataGrid.tsx(탭 형석 그리드) & SewagePipeColumn.ts(컬럼정보)
예시 화면 - 하수도 통계관리/하수관로
2025-12-23 12:05:03 +09:00
dongsilove a828c808ab sn:노후 하수관로 정비 세로스크롤 2025-12-22 22:57:13 +09:00
dongsilove b0c070437f sn: 세로 스크롤 2025-12-22 16:28:11 +09:00
dongsilove 5e9d779639 sn: 노후하수관로정비 모달 수정 2025-12-22 14:45:47 +09:00
rsone001 019415f124 feat: 버튼 컴포넌트 추가 2025-12-22 13:27:43 +09:00
dongsilove b39f7ba614 sn 하수도정비기본계획 fileModal 2025-12-22 13:07:23 +09:00
rsone001 df9cb0a10c fix: build 에러 수정, RealGrid 테스트 key 갱신 2025-12-22 10:43:14 +09:00
swcho cb1bcd6061 feat:
- searchModal 화면 반응형 크기 수정
- 침입수 분석 결과, RDII 분석 결과 표 세로 스크롤 삭제
2025-12-19 18:06:47 +09:00
rsone001 176d0ad089 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-12-18 18:06:50 +09:00
rsone001 c37f4f15f9 st: 기본 셋팅 2025-12-18 17:55:20 +09:00
dongsilove a3ff32e3f8 .env.local 수정 외 2025-12-18 13:38:54 +09:00
jcchoi e64c81be32 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-12-17 18:41:53 +09:00
jcchoi cac1ea4d77 빌드오류 2025-12-17 18:41:35 +09:00
mind-one005 d9bd9ec267 cm: undefined 수정 2025-12-17 18:04:10 +09:00
mind-one005 e751775338 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-12-17 17:38:39 +09:00
mind-one005 a8abeb7e95 cm: ms MView 조회로 수정 2025-12-17 17:38:33 +09:00
jcchoi 86cf1f2e9e Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-12-17 14:39:58 +09:00
jcchoi ae96001488 dashboard modify 2025-12-17 14:39:53 +09:00
mind-one005 2e3e49ccdc cm: 유역지도 연도 2025-12-17 09:36:30 +09:00
mind-one005 1f9003e31f Merge branch 'main' into iipbs/sn/dev 2025-12-16 09:56:33 +09:00
mind-one005 be0f317b7a cm: 오수관로 우수관로 초기화 2025-12-16 09:54:22 +09:00
jcchoi 203c5bc1aa warning 수정 2025-12-15 20:24:33 +09:00
mind-one005 11b009f1a9 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-12-15 20:00:09 +09:00
mind-one005 1aca63020b Merge branch 'iipbs/sn/dev' 2025-12-15 20:00:04 +09:00
mind-one005 504067b125 cm: 지도 3개업무버튼 추가 2025-12-15 19:58:18 +09:00
jcchoi 8599d4b314 반응형 수정정 2025-12-15 19:45:24 +09:00
jcchoi 44bca5c9de 반응형 처리 2025-12-15 19:35:10 +09:00
jcchoi a0574fda6c 대시보드 수정정 2025-12-15 19:21:40 +09:00
jcchoi a7d349391c 수정사항 적용 2025-12-15 17:43:19 +09:00
mind-one005 b05a13b02e cm: reuse-chart 외 디자인 수정 2025-12-15 14:32:16 +09:00
mind-one005 b5f4774ca3 Merge branch 'iipbs/sn/dev' 2025-12-14 21:40:21 +09:00
mind-one005 1cb0393681 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-12-14 21:38:45 +09:00
mind-one005 a5ee56788e Merge branch 'main' into iipbs/sn/dev 2025-12-14 21:34:11 +09:00
mind-one005 02dd3f9355 cm: same update 2025-12-14 21:31:35 +09:00
dongsilove 72ddb5ad9e cm: 지도 수정 2025-12-14 21:25:50 +09:00
ljpark 6ab3d7b1a9 cm: 지도 수정 2025-12-14 18:31:51 +09:00
ljpark ab0daa1eb9 cm:지도 개소수 가동률 2025-12-14 17:30:48 +09:00
jcchoi dc764bbe0d logout 패스 이동동 2025-12-14 15:30:18 +09:00
jcchoi 675b812eac 대시보드 레이아웃 수정정 2025-12-14 14:41:18 +09:00
jcchoi 90ad0afbf6 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-12-14 14:21:18 +09:00
jcchoi ed3df750d1 지원현황 수정정 2025-12-14 14:19:35 +09:00
ljpark 107a6abd77 Merge branch 'main' into iipbs/sn/dev 2025-12-13 19:54:23 +09:00
ljpark 2d82667445 merge 2025-12-13 19:34:40 +09:00
ljpark 373a5b6b09 iipbs/sn/dev merge 2025-12-13 19:34:12 +09:00
ljpark 811ea1560b cm: port 3000 2025-12-13 19:19:12 +09:00
ljpark 8bee745fe8 cm:sn-old-ppl 초기화 외 2025-12-13 18:23:15 +09:00
jcchoi d4424c8fa1 빌드 오류 수정 2025-12-12 19:09:20 +09:00
ljpark 38e41feef2 cm: 더보기, eslint 수정 2025-12-12 18:14:53 +09:00
jcchoi b746db5638 배너 및 지원현황 추가 2025-12-12 18:08:59 +09:00
jcchoi 2ecb9811bd Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-12-12 16:18:09 +09:00
jcchoi 3b2a21f74a 배너 표시 수정정 2025-12-12 16:12:59 +09:00
ljpark d117e1c651 cm:지도유역선택 2025-12-12 15:28:56 +09:00
ljpark fe5ac26033 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2025-12-12 14:51:44 +09:00
ljpark 1bd5da6309 cm:유역변경 2025-12-12 14:51:35 +09:00
swcho a42353467b fix: 개발 서버 에러 수정 테스트2 2025-12-12 14:31:52 +09:00
swcho e34776ccc2 fix: 개발 서버 에러 수정 테스트 2025-12-12 14:13:53 +09:00
swcho c5c5b644d4 Merge remote-tracking branch 'origin/main' into iipbs/sn/dev 2025-12-12 13:17:32 +09:00
jcchoi 352cdde0b5 onlyBuiltDependencies 추가가 2025-12-12 13:06:00 +09:00
jcchoi 55227281a1 타입 오류 수정정 2025-12-12 12:15:00 +09:00
swcho 40cbd827e2 Merge remote-tracking branch 'origin/main' into iipbs/sn/dev 2025-12-12 11:13:15 +09:00
jcchoi ecc0b7a459 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-12-12 11:05:21 +09:00
jcchoi 56d8f7445e 포탈 데시보드 기능 2025-12-12 10:59:59 +09:00
swcho 6ec9e51fb4 chore: pnpm 버전 업데이트 (10.25.0으로) 2025-12-12 10:44:06 +09:00
jcchoi b25e89099d Next.js 15.3.8 업데이트트 2025-12-12 10:35:10 +09:00
ljpark 9306cb6814 cm: 지도 2025-12-11 21:20:09 +09:00
jcchoi 6a3aa5750d 타이틀 제거 2025-12-11 21:12:48 +09:00
jcchoi b04c215503 layout 교정 2025-12-11 21:10:34 +09:00
jcchoi 5ff12b9cbb layout 정리리 2025-12-11 21:02:25 +09:00
ljpark 867f05bed5 Merge branch 'main' into iipbs/sn/dev 2025-12-11 18:09:38 +09:00
ljpark ac7b5d2af2 fo,dm 버전업 후 수정 2025-12-11 18:02:25 +09:00
ljpark b91c026135 cm: 유입방류수질 초기값 2025-12-11 17:03:44 +09:00
jcchoi 654df76793 TypeScript 오류 : JSX.Element를 React.ReactElement로 변경 2025-12-11 16:54:55 +09:00
jcchoi 364491a114 타입스크립트 오류 수정 2025-12-11 16:43:27 +09:00
jcchoi c99c05e199 넥스트 버전 업데이트에 따른 파일 수정 2025-12-11 16:37:30 +09:00
jcchoi f5881b8684 Next.js 버전 업데이트 2025-12-11 14:44:04 +09:00
ljpark dad78579cc cm: 수계별 유입ㆍ방류수질 2025-12-11 14:20:38 +09:00
jcchoi 6d927019dd Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-12-11 13:46:51 +09:00
jcchoi e49954eb41 최신 버전 업데이트 2025-12-11 13:45:15 +09:00
ljpark d3dc7664c2 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-12-10 18:12:42 +09:00
ljpark 11c83dd893 cm: basIn-select 2025-12-10 18:11:05 +09:00
jcchoi 61be6ca339 React 서버 컴포넌트 보안 업데이트 권고 2025-12-10 17:58:52 +09:00
jcchoi e10d246114 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-12-10 17:52:17 +09:00
jcchoi cac963d2d7 React 서버 컴포넌트 보안 업데이트 권고 2025-12-10 17:51:07 +09:00
ljpark 75248dc48f dm: 수계별개소수 가동률 2025-12-10 17:19:02 +09:00
ljpark b440e50cc4 cm: 하수관로현황 2025-12-10 13:45:44 +09:00
ljpark bee0ccad72 cm:물재이용 api 호출 2025-12-09 20:16:35 +09:00
ljpark 34b54c70de 예전 디자인 2025-12-09 10:47:28 +09:00
ljpark b0dbb1110f Merge branch 'main' into iipbs/sn/dev 2025-12-08 19:28:52 +09:00
ljpark 38e266c573 fo,sn,wr,dm : 15.3.6 2025-12-08 15:52:15 +09:00
ljpark 7bf26e07c6 dm:dashboard year 수정 2025-12-08 11:17:59 +09:00
ljpark 431a601171 package.json dev:cm추가 2025-12-05 11:34:39 +09:00
ljpark 3b9e6a9b0d fo: 주석 추가 2025-12-05 09:27:30 +09:00
ljpark f897cbf750 fo: 2021년도 조회시 오류 수정 2025-12-03 14:08:45 +09:00
dongsilove afce96b673 fo: 변수 삭제 2025-12-03 00:11:47 +09:00
dongsilove 75195c7d26 fo: 1회용 변수 삭제 2025-12-02 23:27:42 +09:00
ljpark 65d972f42a wr: 로그인 화면 분기 처리 외 2025-12-02 16:06:10 +09:00
ljpark 34394caaca fo: 지하수 유입량 추정 modal 수정 외 2025-11-27 15:41:55 +09:00
ljpark f68e45d4f4 fo: 유입량 10건씩 조회 2025-11-26 21:18:35 +09:00
ljpark 86417ac7ef fo:원복 외 2025-11-25 17:59:43 +09:00
ljpark f2c1403e3c fo: 재조정 2025-11-25 10:30:21 +09:00
ljpark c46a4a29d5 fo: 처리구역정보 table 너비 조정 2025-11-25 10:25:34 +09:00
dongsilove fc0b74751f fo: table scroll 수정 2025-11-24 20:46:14 +09:00
ljpark 2756fe3490 fo: 오수발생량 모달 깨짐 수정 2025-11-24 15:20:15 +09:00
ljpark 7b844f9e93 fo: 지하수 유입량 추정 2025-11-24 13:01:33 +09:00
ljpark 9a78706abe fo: undefined형 처리 2025-11-24 10:14:18 +09:00
ljpark 43af6e5259 fo:물사용량 조회 api추가 2025-11-23 22:08:37 +09:00
ljpark 25504bb781 fo:오수발생량 추정 수정 2025-11-23 20:36:57 +09:00
ljpark c2bc07e679 Merge branch 'iipbs/sn/dev' 2025-11-18 18:04:54 +09:00
ljpark 2f8a31c670 Merge branch 'iipbs/wr/dev' into iipbs/sn/dev 2025-11-18 16:48:29 +09:00
ljpark 43c2b32dc2 Merge remote-tracking branch 'origin/main' into iipbs/sn/dev 2025-11-18 16:48:10 +09:00
ljpark 8c40943d9c fo: 강우data promise.all 제거 2025-11-18 16:35:28 +09:00
ljpark 9dfdbe224e Merge branch 'iipbs/wr/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/wr/dev 2025-11-17 10:35:16 +09:00
ljpark 37ead70b80 wr: 로그인 처리 2025-11-17 10:33:38 +09:00
jcchoi d6f29f978d tinymce 에디터 한글화 2025-11-14 18:14:37 +09:00
jcchoi 85c1a7a5e3 도움말템플릿릿 2025-11-14 14:17:59 +09:00
jcchoi 5d91df405f 도움말 템플릿 및 이미지원복복 2025-11-14 14:17:46 +09:00
jcchoi 623fd870c6 헬프 이미지 변경 및 위치 변경 2025-11-14 11:36:00 +09:00
jcchoi 562d691500 help 이미지 색상상 수정 2025-11-14 09:59:20 +09:00
dongsilove dbebe6313e wr 개발 로그인 페이지 추가 2025-11-13 22:11:27 +09:00
jcchoi b4d4f947e2 도움말 아이콘 및 수정 2025-11-13 21:40:53 +09:00
jcchoi 2edfee8890 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-11-13 18:40:48 +09:00
jcchoi ca37374c87 메뉴얼 팝업 공통화 추가 2025-11-13 18:30:43 +09:00
ljpark 77ce8b8100 wr:home page.tsx 수정중 2025-11-13 18:24:26 +09:00
swcho 65727cdddc Merge branch 'iipbs/sn/dev' 2025-11-13 18:04:17 +09:00
swcho e30e73c29b feat: 중점우수토실 운영정보 길이가 길 경우 스크롤 추가 2025-11-13 18:03:57 +09:00
ljpark 54d20f6564 첫페이지 이동 2025-11-13 17:43:55 +09:00
jcchoi 3d15def3ad Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-11-13 17:27:12 +09:00
jcchoi 530c1161dd 그린테마 적용시 글자색 변경경 2025-11-13 17:27:08 +09:00
swcho 087315c510 Merge branch 'iipbs/sn/dev' 2025-11-13 15:39:59 +09:00
swcho 462cce13a8 fix: 버튼 style 수정 2025-11-13 15:39:44 +09:00
swcho ba80c191fb fix: 빌드 에러 수정 2025-11-13 15:23:44 +09:00
swcho 4912823293 fix: 물재이용 주소 검색 수정 반영 2025-11-13 15:14:49 +09:00
swcho ddbaa2fdd2 fix: 대민 주소검색 수정 2025-11-13 14:08:43 +09:00
swcho 5ccec85086 fix: 대민 주소검색 수정 2025-11-13 14:01:25 +09:00
dongsilove deb9bcc8cf wr: 로그인 화면 2025-11-12 23:30:26 +09:00
ljpark ab1edc4adc Merge branch 'iipbs/sn/dev' 2025-11-12 18:12:45 +09:00
ljpark 20ab392ee7 Merge branch 'iipbs/dm/dev' into iipbs/sn/dev 2025-11-12 17:54:46 +09:00
ljpark 029340220e dm: 수요처 등록시 user session저장 2025-11-12 17:54:13 +09:00
ljpark 596c532c31 Merge branch 'iipbs/fo/dev' into iipbs/sn/dev 2025-11-12 16:43:05 +09:00
ljpark 8f201676f8 fo: rdii gugun으로 시군구 수정 2025-11-12 16:42:30 +09:00
ljpark e9d1c06b44 Merge branch 'iipbs/fo/dev' into iipbs/sn/dev 2025-11-12 16:14:20 +09:00
ljpark af6fd7b6c5 wr: 표준공사비 영역 분리 2025-11-12 16:01:29 +09:00
ljpark 8259b3829a 강수현황 exceldown 수정 2025-11-12 15:10:51 +09:00
jcchoi d912ca8c6e Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-11-12 14:05:51 +09:00
swcho 46049ac3c4 feat: 중점우수토실 세로 간격 조정 (공단 페이지에서 아래가 짤리는 문제가 있음) 2025-11-12 13:19:47 +09:00
dongsilove 81b027cfe1 fo 수정중2 2025-11-12 00:00:18 +09:00
ljpark 07ecc1de7b Merge branch 'iipbs/fo/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/fo/dev 2025-11-11 18:11:34 +09:00
ljpark 178b8bd782 fo 작업중 2025-11-11 18:11:30 +09:00
swcho 084f08c516 fix: 중점우수토실 강우/측정 정보 줄 바꿈 에러로 인한 간격 조정 (연월일 시분초 -> 월일 시분) 2025-11-11 17:50:41 +09:00
swcho 0a0ffd5f21 fix: - 중점우수토실 운영정보 호출 API 수정
- Search Modal Tab 띄어쓰기
2025-11-11 16:49:44 +09:00
swcho a7ab57b5eb fix: 하수관로 레이어 호출 api 수정 2025-11-11 15:43:21 +09:00
swcho fc89da8bef feat:
- sn API 호출 데이터 타입 수정
- rdiiChart 옵셔널 체이닝(Optional Chaining)으로 수정
2025-11-10 18:07:04 +09:00
swcho 2475d460d3 chmod: location -> window.location 수정 2025-11-10 13:53:49 +09:00
ljpark 60bd74aa46 Merge branch 'iipbs/wr/dev' into iipbs/sn/dev 2025-11-10 13:26:44 +09:00
ljpark c80af1efcb wr: 수요매칭 자동 스크롤 2025-11-10 11:10:00 +09:00
swcho 68c84588b7 fix: API 경로 수정 2025-11-07 14:02:54 +09:00
swcho 87fed1d59c Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2025-11-07 10:50:56 +09:00
swcho 77ef7916f2 feat: 중수도시설, 온배수시설 삭제 버튼 구현 2025-11-07 10:50:43 +09:00
swcho 0088e01d38 feat: 빗물이용시설 삭제 버튼 구현 2025-11-07 10:32:42 +09:00
dongsilove ef5e927f0d Merge branch 'iipbs/dm/dev' into iipbs/sn/dev 2025-11-06 20:52:30 +09:00
ljpark debba14e13 fo: rdii 오류 수정 2025-11-06 16:05:36 +09:00
ljpark f0c48a1a02 Merge branch 'iipbs/fo/dev' into iipbs/sn/dev 2025-11-06 15:15:26 +09:00
ljpark 77616a738a rdii 수정 2025-11-06 15:12:42 +09:00
jcchoi 25ab247588 쿠키 삭제제 2025-11-06 10:13:50 +09:00
swcho 8e156485c3 feat, fix: - 로그아웃 로직 추가(수정중)
- 로그인 에러 수정
2025-11-06 09:32:01 +09:00
ljpark 8a57e5bda3 Merge branch 'main' into iipbs/dm/dev 2025-11-05 21:33:40 +09:00
ljpark 598dfc7b78 dm:수요처 위치가 없는 경우 alert 2025-11-05 21:33:05 +09:00
swcho c1a7dd946c chmod: 오타 수정 (중축 -> 증축) 2025-11-05 16:52:19 +09:00
swcho b011644d38 feat: 로그인 분기 처리 2025-11-05 10:25:13 +09:00
swcho 6a50f12d89 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2025-11-05 09:32:42 +09:00
swcho 3073bbdc18 feat: 이력추가시 최하단으로 스크롤 이동 로직 추가 2025-11-05 09:32:02 +09:00
swcho 2ad6ec2ab0 chmod: 오타 수정 (중축 -> 증축) 2025-11-05 09:19:57 +09:00
ljpark 17bf078de8 침입수 RDII 목록조회시 AUTH제거 2025-11-04 16:25:26 +09:00
mind-one003 7371c16928 도시코드가 0인 데이터 필터링 2025-11-03 17:43:12 +09:00
swcho a1a21df3ae chmod: 빗물, 중수도, 온배수 시설 등록 페이지 시군구 표기 수정 (" " -> "-") 2025-11-03 15:24:18 +09:00
swcho d9a1e235fb chmod: 빗물, 중수도, 온배수 시설 등록 페이지 시군구 표기 수정 ("-" -> " ") 2025-11-03 15:21:46 +09:00
swcho e111fab241 fix:
- 빗물, 중수도, 온배수 시설등록 페이지 세종특별자치시 "전체", "없음" 표시 에러 수정
- bdstReg 신규 등록 시 세종특별자치시 에러 수정
- rainSearch 주석 삭제
2025-11-03 14:54:43 +09:00
swcho f5018e8689 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2025-11-03 11:19:03 +09:00
dongsilove aa973f59a3 dm dashboard chart 오류 수정 2025-11-02 15:49:54 +09:00
mind-one003 a156001d03 오류수정- label이 없는 데이터를 차트데이터에 넣어 max값을 추출하지 못함 2025-11-02 12:31:21 +09:00
mind-one003 db0aa001a1 initTrigger 미작성 코드 사용 2025-11-01 11:07:28 +09:00
mind-one003 618dd0c2dd 빌드에러수정 2025-11-01 11:01:29 +09:00
mind-one003 56191e44d8 Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs/fo/dev 2025-11-01 01:42:02 +09:00
mind-one003 fd096f9d84 1.유량현황, 수질현황, rdii, 계획하수량 서울조회조건 추가
2.처리구역정보 api신규추가 (한페이지 시설코드리스트로 api 호출)
2025-11-01 01:41:32 +09:00
ljpark 4298c83791 Merge branch 'main' into iipbs/sn/dev 2025-10-31 15:30:16 +09:00
ljpark c1cc7e846f wr 하수재이용실적등록 목록 api 수정 2025-10-31 15:01:20 +09:00
swcho 921198060d Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2025-10-31 09:44:45 +09:00
mind-one003 09289b1622 처리구역정보 원복 2025-10-30 19:05:08 +09:00
mind-one003 e2d040dc4a Merge commit '5bc4de6d8a7225b3c5815cfa79f16d5d6d13c423' into iipbs/fo/dev 2025-10-30 18:41:31 +09:00
mind-one003 d3771d226b 원복 2025-10-30 18:40:38 +09:00
ljpark 5bc4de6d8a Merge remote-tracking branch 'origin/iipbs/dm/dev' into iipbs/sn/dev 2025-10-30 17:22:18 +09:00
leedj312 2ad8a26c78 대민 비고 오른쪽 입력 삭제 2025-10-30 16:35:38 +09:00
mind-one003 5898f99c59 Merge branch 'iipbs/sn/dev' into iipbs/wr/dev 2025-10-30 16:20:24 +09:00
mind-one003 3c6d10a5e3 중수도통계, 빗물시설 통계 건수 추출 수정(추후에 공통으로 바로 바라볼수있도록 수정 필요할듯?) 2025-10-30 16:19:21 +09:00
leedj312 2a917e4e2d 물재이용 : 실적 등록 페이지 코드없을시 조회안하도록 설정 2025-10-30 11:34:43 +09:00
mind-one003 960e5f7dd1 작업 진행중 2025-10-30 11:03:14 +09:00
swcho 738489a34c chmod: SearchModal 탭 '운영정보' -> '유지관리 정보'로 명칭 수정 (고객 요청) 2025-10-30 10:42:20 +09:00
ljpark 89f14ff910 Merge remote-tracking branch 'origin/iipbs/sn/dev' 2025-10-29 19:53:39 +09:00
leedj312 a0dd2f9200 Merge commit 'd2dca757a7dc8a51a2fc7f5ff6cac48779991ff8' into iipbs/sn/dev 2025-10-29 19:36:45 +09:00
leedj312 d2dca757a7 물재이용: 지도점검 조회 코드 로그인 코드로 수정 2025-10-29 19:36:04 +09:00
ljpark 0ac94449ea 대민 후보지조회 수정 2025-10-29 19:04:17 +09:00
mind-one003 231d5ba01b Merge branch 'iipbs/sn/dev' into iipbs/fo/dev 2025-10-29 17:44:32 +09:00
mind-one003 c701116167 주석추가, 테이블 헤더 수정 2025-10-29 17:43:45 +09:00
ljpark 59592bf89b 물재이용 2025-10-29 17:31:06 +09:00
leedj312 f92a900c5d Merge commit 'b61c7ea9c4230ac0b16948cee666b547b9ebcff2' into iipbs/sn/dev 2025-10-29 16:27:55 +09:00
ljpark 89ff128f17 Merge remote-tracking branch 'origin/iipbs/sn/dev' 2025-10-29 15:33:55 +09:00
leedj312 50e2ada750 uuid 없을시 임의 uuid 적용 2025-10-29 15:14:17 +09:00
rsone001 4e3413484d fix: 연계 등록 form 수정 2025-10-29 14:56:14 +09:00
leedj312 b61c7ea9c4 대민 주소검색 false처리 2025-10-29 14:49:21 +09:00
leedj312 f8102ec13d 건축물 등록시 mdfr rgtr 로그인한 사용자의 uuid를 입력할수 있도록 수정 2025-10-29 11:22:30 +09:00
leedj312 02de87264f 공통 컴포넌트: 수요처 등록시 rgtr 수정시 mdfr 입력되도록 수정 2025-10-29 11:21:42 +09:00
leedj312 4fbb3a50b8 수요처 관리 : 수요처 업데이트시 mdfr 필드 업데이트 되도록 수정 2025-10-29 11:07:21 +09:00
leedj312 631ffa1727 로그인한 사용자의 uuid 찾기 2025-10-29 11:06:40 +09:00
leedj312 231c8a213e 지도점검 엑셀 오류 및 변경이력 삭제 2025-10-28 18:22:59 +09:00
leedj312 ede1e14a03 물재이용: 업데이트시 테이블 목록 초기화 2025-10-28 16:35:24 +09:00
leedj312 b503a6a60e Merge commit '8b8b2a03e2c81bf38abda2986018530dd6448e72' into iipbs/wr/dev 2025-10-28 16:33:16 +09:00
mind-one003 8b8b2a03e2 Merge branch 'iipbs/fo/dev' into iipbs/sn/dev 2025-10-27 20:53:03 +09:00
mind-one003 236572370a 엑셀 다운로드 2025-10-27 20:52:26 +09:00
ljpark a30be18e56 Merge branch 'main' into iipbs/sn/dev 2025-10-27 20:27:57 +09:00
ljpark 762c97d186 page 추가 2025-10-27 20:26:05 +09:00
leedj312 64fdf2102d 물재이용: 빗물이용 시설 내용 업데이트 시 테이블 index 조정 2025-10-27 19:16:34 +09:00
jcchoi 0fd1fa74ba 우편번호호 2025-10-27 17:53:55 +09:00
mind-one003 4dc31882a6 rdii, 오수발생량, 지하수유입량 추정 속도 및 다운로드 개선 2025-10-27 15:03:50 +09:00
jcchoi 6c2223a021 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-10-27 10:11:42 +09:00
jcchoi d89b157999 도메인 추가 2025-10-27 10:11:36 +09:00
ljpark e48196e351 Merge branch 'iipbs/sn/dev' 2025-10-27 09:47:07 +09:00
ljpark 57a4fa54eb fo 443,443 2025-10-27 09:45:26 +09:00
mind-one003 07b65a9006 빌드에러수정 2025-10-26 22:31:17 +09:00
mind-one003 57cca02d65 Merge branch 'iipbs/fo/dev' into iipbs/sn/dev 2025-10-26 22:19:47 +09:00
mind-one003 b5e03c2430 지하수유입량 추정, 오수발생량 추정 속도개선 2025-10-26 22:13:07 +09:00
yschoi 2e01068398 geoserver url 헤더 토큰 추가 2025-10-26 17:36:24 +09:00
dongsilove 4272a5c944 wr 하수재이용통계 2025-10-25 15:03:00 +09:00
dongsilove e71c50e9ad fix: wr 하수재이용통계 2025-10-24 23:42:03 +09:00
ljpark 1e14b464a4 하수재이용통계 2025-10-24 19:40:39 +09:00
ljpark 05f25d27f4 local 테스트 주석 추가 2025-10-24 16:33:16 +09:00
leedj312 800f3e4943 물재이용 등록 부분 수정 2025-10-24 15:21:38 +09:00
swcho 6f8509d39f Merge branch 'iipbs/sn/dev' 2025-10-24 10:33:52 +09:00
swcho 8dfda1aa15 fix: fetchDcTree 함수에 "기본계획" 파라미터 추가 2025-10-24 10:33:29 +09:00
ljpark a3f2822cfe Merge branch 'iipbs/sn/dev' 2025-10-23 20:22:04 +09:00
ljpark 53beadef9d dm middleware 수정 2025-10-23 20:10:10 +09:00
swcho 4fb40e4024 Merge remote-tracking branch 'origin/iipbs/sn/dev' 2025-10-23 17:53:36 +09:00
swcho 5d73f91ed4 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2025-10-23 16:44:43 +09:00
swcho c999daa23f feat: sn, wr, fo 로그아웃 적용 2025-10-23 16:44:34 +09:00
ljpark f4d1c7b4f6 ngii_url 수정 누락 2025-10-23 13:23:21 +09:00
ljpark b161435a91 sewerage-service/api api제거 2025-10-23 13:09:47 +09:00
ljpark 2106762987 sewerage-service/api api제거 2025-10-23 11:38:32 +09:00
ljpark 470fc6f3f0 sn지도확인-나머지 동일하게 수정 2025-10-22 17:44:38 +09:00
ljpark bfbec06993 fo lyrStngs 수정 2025-10-22 15:05:04 +09:00
swcho 29b3f4a6a5 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2025-10-22 14:45:43 +09:00
swcho 976b3149d9 fix: SN NGII 배경지도 호출 오타 수정 2025-10-22 14:45:42 +09:00
ljpark b90f6137cc tempUrl 삭제 2025-10-22 13:03:00 +09:00
ljpark 48038b9ea0 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2025-10-22 12:56:48 +09:00
ljpark b8a8ca4126 임시 commit 2025-10-22 12:56:42 +09:00
swcho 4388dc5862 Merge branch 'iipbs/sn/dev' 2025-10-22 10:40:46 +09:00
swcho 7457e7d101 chmod: auth-wrapper.tsx pathname 삭제 (추후 다시 추가 필요) 2025-10-22 10:40:22 +09:00
dongsilove a14a652aec Merge branch 'iipbs/sn/dev' 2025-10-21 22:52:35 +09:00
swcho 4c07b1b044 fix: dc url 추가 2025-10-21 19:20:02 +09:00
swcho 661959a8d8 fix: fo env 파일 수정 2025-10-21 19:16:16 +09:00
swcho 52301a65bd Merge branch 'iipbs/sn/dev' 2025-10-21 19:07:59 +09:00
swcho 26bd781a99 fix: middleware.ts 수정 2025-10-21 19:01:43 +09:00
swcho c3e4273d0c fix: 침입수 분석, RDII 분석 에러 수정 (middleware proxy Timeout 설정 추가) 2025-10-21 18:56:35 +09:00
swcho 02f51a045d Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2025-10-21 18:08:56 +09:00
swcho 77f67b84ae fix: 도시침수예방사업, 노후하수관로 정비사업 에러 수정 2025-10-21 18:08:46 +09:00
ljpark 1bd29f5216 ngii all 2025-10-21 18:08:41 +09:00
ljpark f27074b8e6 ngii sn 2025-10-21 17:51:40 +09:00
swcho 85f5c730eb chmod: 로그인 권한 체크 주석 처리 2025-10-21 16:57:10 +09:00
swcho 3f28845a7b fix: 물재이용 유량현황 조회 에러 수정 2025-10-21 16:37:22 +09:00
swcho 45ea8d30f6 fix:
- pnpm-lock.yaml 롤백
- 빌드 에러 수정
2025-10-21 15:41:15 +09:00
swcho bc39549bcc fix: sn 커밋 롤백 2025-10-21 15:17:19 +09:00
swcho 2c30de2824 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2025-10-21 15:11:33 +09:00
swcho 0585a6d8df fix: 운영실태(fo), 물재이용(wr) 프록시 라우트 파일 추가 2025-10-21 15:11:12 +09:00
ljpark 4259246efb /dc-service , /dc/file 수정 2025-10-21 10:29:50 +09:00
dongsilove 6cf389c76f wr lyStngs 호출 수정 2025-10-20 21:04:38 +09:00
ljpark 06fe6494a9 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2025-10-20 19:11:02 +09:00
ljpark c0af2f9f9b dc-service 주소 변경 2025-10-20 19:10:56 +09:00
dongsilove 77dd04a140 col,row 변경 2025-10-17 22:28:21 +09:00
dongsilove a5a0e8a89e domain-devqueen.hasuon...아직 2025-10-17 22:21:50 +09:00
ljpark 250c4239c9 login url 수정 2025-10-17 18:51:58 +09:00
ljpark 772bb39341 ngii주소변경 2025-10-17 18:16:58 +09:00
swcho e7f86e84c2 feat: wr, fo, dm favicon sn과 동일하게 설정 2025-10-17 17:42:36 +09:00
ljpark 89bb35bb3c wr cookie 외 2025-10-17 17:29:26 +09:00
ljpark 0f9d13a80a Merge remote-tracking branch 'origin/main' into iipbs/sn/dev 2025-10-17 17:17:06 +09:00
jcchoi 0bfa3dcb2c Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-10-17 15:38:04 +09:00
jcchoi 9c9ddbfcd7 빌드 오류 수정 2025-10-17 15:38:01 +09:00
rsone009 c23a19d255 자료관 DC서비스 추가 2025-10-17 11:52:42 +09:00
ljpark e14b12a955 Merge remote-tracking branch 'origin/main' into iipbs/sn/dev 2025-10-17 11:12:06 +09:00
jcchoi 44d04d7532 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-10-17 09:43:22 +09:00
jcchoi 8330c0168e Cookie Domain 2025-10-17 09:43:18 +09:00
mind-one005 347892c9d7 Merge branch 'main' into iipbs/sn/dev 2025-10-16 22:23:49 +09:00
mind-one005 c8224912f6 setCookie원복 2025-10-16 22:23:12 +09:00
mind-one005 e11a761f18 .env 수정 2025-10-16 21:43:40 +09:00
rsone001 2f04f5c53e Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-10-16 13:49:52 +09:00
rsone001 ceacb13704 fix: 외부 API 연계 데이터 연계 key 만료 체크 기능 추가 2025-10-16 13:47:43 +09:00
swcho 2887cb5c35 feat: 쿠키 설정에 도메인 추가 2025-10-15 18:10:35 +09:00
ljpark a1a83ff6aa Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2025-10-15 15:49:33 +09:00
ljpark 90b03e283a CM_LOGIN_PAGE 추가 2025-10-15 15:49:27 +09:00
swcho 4a008432ad Merge branch 'iipbs/fo/dev' 2025-10-15 13:48:42 +09:00
swcho ca27e863f9 Merge branch 'iipbs/wr/dev' 2025-10-15 13:48:34 +09:00
swcho 9984059f9d fix: 권한 체크를 최초 실행시만이 아니라 pathname 변경시로 변경 2025-10-15 13:48:08 +09:00
swcho 70a9a0b186 fix: 권한 체크를 최초 실행시만이 아니라 pathname 변경시로 변경 2025-10-15 13:43:13 +09:00
swcho 4bcef0fff1 fix, remove:
- 권한 체크를 최초 실행시만이 아니라 pathname 변경시로 변경.
- hostname 로그 삭제
2025-10-15 13:37:13 +09:00
swcho b3480556ee chmod: 개발 서버 경로 추가 2025-10-15 13:07:42 +09:00
swcho 82d29e26f7 chmod: 테스트용 로그 추가 2025-10-15 13:03:17 +09:00
swcho 05e60d5582 Merge branch 'main' into iipbs/sn/dev 2025-10-15 11:39:47 +09:00
ljpark 3b7d217d0e dc-service middleware 수정 2025-10-15 11:17:05 +09:00
swcho c9449fd608 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-10-15 10:17:18 +09:00
swcho 0aab801284 feat:
- 로그인 로직 local, vdi 운영 환경 URL로 분기 처리
- 백엔드 URL 게이트웨이 경로로 수정
2025-10-15 10:16:32 +09:00
mind-one005 c84a463d2c sn ngii_url 수정 2025-10-14 22:06:10 +09:00
ljpark 10426e7a4b Merge branch 'iipbs/fo/dev' 2025-10-14 17:59:50 +09:00
ljpark 398a112030 apigateway로 수정 2025-10-14 17:46:33 +09:00
jcchoi 98c9880300 pnpm-lock.yaml 추가 2025-10-13 19:02:40 +09:00
jcchoi 27586a6960 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-10-13 19:02:16 +09:00
jcchoi fb23055cfc Tinymce 에디터 폐쇄망 적용 2025-10-13 18:54:06 +09:00
swcho 1e26ce8402 Merge branch 'iipbs/sn/dev' 2025-10-13 17:48:57 +09:00
swcho f953b412fb Merge remote-tracking branch 'origin/iipbs/fo/dev' 2025-10-13 17:48:34 +09:00
swcho 94c3d9b394 fix: start:fo -> start로 스크립트 롤백 2025-10-13 17:41:29 +09:00
swcho ad11592ae9 fix: start:fo 스크립트 포트 삭제 2025-10-13 17:32:44 +09:00
swcho 29fd2f551d Merge branch 'iipbs/fo/dev' 2025-10-13 17:22:30 +09:00
swcho 4f54a22c74 Merge branch 'iipbs/fo/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/fo/dev 2025-10-13 17:22:14 +09:00
swcho 85eae85487 add: start:fo 스크립트 추가 2025-10-13 17:22:10 +09:00
ljpark 01fb739587 .env.mindone 수정 2025-10-13 17:07:38 +09:00
swcho 7f1cd734d9 Merge branch 'iipbs/wr/dev' 2025-10-13 16:45:24 +09:00
swcho 8ba320eb0c add: pacakage.json 라이브러리 추가 2025-10-13 16:44:19 +09:00
ljpark cceedeb880 package.json 3 2025-10-13 15:40:06 +09:00
ljpark 604c7b9a60 package.json 2 2025-10-13 15:38:04 +09:00
ljpark 43246c33be package.json수정 2025-10-13 15:30:22 +09:00
swcho eeb16fbe51 chmod: env파일 수정 및 주석 추가 2025-10-13 15:07:04 +09:00
swcho 329e9da2b3 Merge branch 'main' into iipbs/sn/dev 2025-10-13 10:08:45 +09:00
swcho ba83dbb072 fix: fo, wr 빌드 에러 수정 2025-10-13 09:47:55 +09:00
mind-one005 4c656930e0 Merge branch 'main' into iipbs/dm/dev 2025-10-11 11:30:23 +09:00
dongsilove 892e439c51 대민 수정 외 .env.mindone추가 2025-10-10 17:56:08 +09:00
swcho d2bc79983e Merge branch 'iipbs/wr/dev' 2025-10-02 15:23:17 +09:00
swcho 5a80b42c9c chmod: 루트 페이지 리다이렉트 로직 추가 후 주석 처리 (추후 사용) 2025-10-02 15:19:48 +09:00
jcchoi 3e97438638 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-10-02 14:40:55 +09:00
jcchoi f3461e8fbe 쿠키 설정정 2025-10-02 14:40:49 +09:00
swcho 409e47b1a5 Merge remote-tracking branch 'origin/iipbs/fo/dev' 2025-10-02 14:00:43 +09:00
swcho ba468c9224 feat: 쿠키 통한 로그인 권한 체크 구현 2025-10-02 14:00:03 +09:00
ljpark 9e4d127942 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-10-02 13:51:53 +09:00
ljpark 7cb4f624ee 대민 미들웨어 경유 2025-10-02 13:51:48 +09:00
swcho fb0178b2a8 feat: 쿠키 통한 로그인 권한 체크 구현 2025-10-02 11:44:43 +09:00
ljpark 5e3f5e6aa8 미들웨어 경유 2025-10-01 20:51:15 +09:00
ljpark e79e8e2251 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2025-09-30 18:14:21 +09:00
ljpark 01c9752022 대민 운영 .env 수정 2025-09-30 18:14:16 +09:00
swcho db8f8896d7 Merge branch 'main' into iipbs/sn/dev 2025-09-30 15:45:24 +09:00
leedj312 10aa7431bc 대민 시도 코에 없는 시군구 표출x 2025-09-28 19:02:33 +09:00
1354 changed files with 161449 additions and 15741 deletions
+7
View File
@@ -21,3 +21,10 @@ apps/fo/.next/**/*
# env
#.env*
.qodo
pnpm-lock.yaml
settings.json
.npmrc
.cursor
apps/**/tsconfig.tsbuildinfo
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
BRANCH=$(git rev-parse --abbrev-ref HEAD)
pnpm turbo run build --filter="...[origin/$BRANCH]"
+5
View File
@@ -0,0 +1,5 @@
# apps/as/shared/config/env.ts 참고
NEXT_PUBLIC_PORT=
NEXT_PUBLIC_PROXY_HOST=
NEXT_PUBLIC_SITE_ID=
NEXT_PUBLIC_SERVER_API_URL=
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
+36
View File
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
+8
View File
@@ -0,0 +1,8 @@
{
"semi": true,
"trailingComma": "all",
"singleQuote": false,
"printWidth": 100,
"tabWidth": 2,
"plugins": ["prettier-plugin-tailwindcss"]
}
+248
View File
@@ -0,0 +1,248 @@
# AS Application
Next.js 애플리케이션 with FSD(Feature Sliced Design) 아키텍처
## 기술 스택
- **프레임워크**: Next.js 15 + TypeScript
- **스타일링**: Tailwind CSS
- **상태관리**: Zustand
- **데이터 페칭**: TanStack Query (React Query)
- **폼 관리**: React Hook Form + Zod
- **테이블**: TanStack Table
- **HTTP 클라이언트**: Axios
- **날짜 처리**: Day.js
## 프로젝트 구조 (FSD)
이 프로젝트는 **Feature-Sliced Design v2.1**을 따르며, 계층적 아키텍처로 구성되어 있습니다.
```
as/
├── app/ # App 레이어 (Next.js App Router)
│ ├── (routes)/ # 라우트 구조
│ ├── layout.tsx # 루트 레이아웃
│ ├── page.tsx # 홈 페이지
│ ├── globals.css # 글로벌 스타일
│ └── providers.tsx # 전역 providers
│
├── widgets/ # Widgets 레이어
│ └── [widget-name]/ # 큰 독립적 UI 블록 (여러 페이지에서 재사용)
│ ├── ui/
│ ├── model/
│ └── index.ts
│
├── features/ # Features 레이어
│ └── [feature-name]/ # 사용자 상호작용 기능 (여러 페이지에서 재사용)
│ ├── ui/
│ ├── api/
│ ├── model/
│ └── index.ts
│
├── entities/ # Entities 레이어
│ └── [entity-name]/ # 비즈니스 엔티티 (User, Product 등)
│ ├── model/
│ ├── api/
│ ├── ui/
│ └── index.ts
│
└── shared/ # Shared 레이어
├── ui/ # UI Kit (비즈니스 로직 없는 공통 컴포넌트)
├── api/ # API 클라이언트 설정
├── lib/ # 내부 라이브러리 (dates, colors 등)
├── config/ # 환경 설정, feature flags
├── types/ # 공통 타입 정의
└── routes/ # 라우트 상수
```
### 계층 구조 이해하기
FSD는 **3단계 계층**으로 구성됩니다:
1. **Layers (레이어)**: 책임과 의존성에 따른 수평적 분리 (app, pages, widgets, features, entities, shared)
2. **Slices (슬라이스)**: 비즈니스 도메인에 따른 수직적 분리 (user, product, auth-login 등)
3. **Segments (세그먼트)**: 기술적 목적에 따른 분리 (ui, api, model, lib 등)
> 📚 각 레이어의 자세한 설명은 해당 폴더의 README.md를 참고하세요.
## FSD 레이어 설명
FSD는 계층적 아키텍처로, 각 레이어는 책임의 범위와 의존성의 정도에 따라 구분됩니다. 7개의 표준화된 레이어가 있으며, 프로젝트에 필요한 레이어만 사용하면 됩니다.
### 레이어 계층 구조 (위에서 아래로)
```
app/ ↑ 가장 높은 책임, 가장 많은 의존성
pages/ |
widgets/ |
features/ |
entities/ |
shared/ ↓ 가장 낮은 책임, 의존성 없음
```
### 1. App
- **목적**: 앱 전체에 영향을 주는 기술적/비즈니스적 사항
- **내용**: Context providers, 글로벌 스타일, 스토어 설정, 라우터 설정, 애널리틱스
- **특징**: 슬라이스가 없고 세그먼트로만 구성
- **의존성**: 모든 하위 레이어 사용 가능
### 2. Pages (app 디렉토리)
- **목적**: 웹사이트나 애플리케이션을 구성하는 페이지/화면
- **내용**: 페이지 UI, 로딩 상태, 에러 바운더리, 데이터 fetching
- **특징**: 한 페이지당 한 슬라이스 (유사한 페이지는 그룹화 가능)
- **의존성**: `widgets`, `features`, `entities`, `shared` 사용 가능
> 💡 **FSD v2.1 권장사항**: "pages-first" 접근 방식을 따라, 재사용되지 않는 큰 UI 블록과 비즈니스 로직은 페이지 슬라이스에 유지하세요.
### 3. Widgets
- **목적**: 큰 규모의 독립적인 UI 블록
- **내용**: 여러 features와 entities를 조합한 복합 UI
- **사용 시기**: 여러 페이지에서 재사용되거나, 페이지에 여러 개의 큰 독립적 블록이 있을 때
- **의존성**: `features`, `entities`, `shared` 사용 가능 (다른 `widgets` 불가)
### 4. Features
- **목적**: 사용자가 수행하는 주요 상호작용과 비즈니스 기능
- **내용**: 사용자 시나리오 (예: 로그인, 상품 검색, 댓글 작성)
- **사용 시기**: 여러 페이지에서 재사용되는 기능
- **의존성**: `entities`, `shared` 사용 가능 (다른 `features` 불가)
### 5. Entities
- **목적**: 비즈니스가 다루는 실제 개념 (User, Product, Order 등)
- **내용**: 데이터 모델, 타입, API 요청, 엔티티의 시각적 표현
- **특징**: 비즈니스 용어로 명명
- **의존성**: 다른 `entities`, `shared` 사용 가능
### 6. Shared
- **목적**: 애플리케이션의 기반 제공
- **내용**: API 클라이언트, UI Kit, 유틸리티 라이브러리, 글로벌 설정
- **특징**: 비즈니스 로직 없음, 슬라이스가 없고 세그먼트로만 구성
- **의존성**: 외부 라이브러리만 사용 가능 (다른 레이어 불가)
## 레이어 간 Import 규칙
> **핵심 규칙**: 슬라이스 내의 모듈(파일)은 **오직 하위 레이어**에 있는 다른 슬라이스만 import할 수 있습니다.
예시:
- `features/auth-login/ui/form.tsx`는 `entities/user`와 `shared/ui`를 import 가능
- 하지만 다른 `features/post-create`나 상위 레이어인 `widgets`는 import 불가
- 같은 슬라이스 내부(`features/auth-login/`)의 다른 파일들은 자유롭게 import 가능
## Public API 규칙
> **슬라이스 외부의 모듈은 슬라이스의 Public API만 참조할 수 있으며, 내부 파일 구조에 직접 접근할 수 없습니다.**
모든 슬라이스(및 슬라이스가 없는 레이어의 세그먼트)는 반드시 `index.ts`를 통해 Public API를 정의해야 합니다.
```typescript
// ✅ 올바른 import
import { LoginForm } from 'features/auth-login';
// ❌ 잘못된 import (내부 구조 직접 접근)
import { LoginForm } from 'features/auth-login/ui/login-form';
```
## 시작하기
```bash
# 의존성 설치
pnpm install
# 개발 서버 실행
pnpm run dev
# 프로덕션 빌드
pnpm run build
# 프로덕션 서버 실행
pnpm start
```
## 환경 변수
`.env.local` 파일 생성:
```env
NEXT_PUBLIC_API_URL=http://localhost:3000/api
```
## FSD 핵심 규칙
### 1. Import 규칙
> **슬라이스 내의 모듈(파일)은 오직 하위 레이어에 있는 슬라이스만 import할 수 있습니다.**
```typescript
// ✅ 올바른 import (하위 레이어)
// features/auth-login/ui/form.tsx
import { User } from 'entities/user';
import { Button } from 'shared/ui/button';
// ❌ 잘못된 import (같은 레이어)
// features/auth-login/ui/form.tsx
import { PostForm } from 'features/post-create'; // 금지!
// ❌ 잘못된 import (상위 레이어)
// features/auth-login/ui/form.tsx
import { Header } from 'widgets/header'; // 금지!
```
### 2. Public API 규칙
> **슬라이스 외부에서는 Public API(index.ts)를 통해서만 접근해야 합니다.**
```typescript
// ✅ 올바른 import (Public API 사용)
import { LoginForm } from 'features/auth-login';
// ❌ 잘못된 import (내부 구조 직접 접근)
import { LoginForm } from 'features/auth-login/ui/login-form';
```
**Public API 작성 시 주의사항**:
- 와일드카드 re-export(`export * from`) 사용 지양
- 필요한 것만 명시적으로 export
- 슬라이스의 인터페이스를 명확하게 표현
```typescript
// ❌ 나쁜 예시
export * from './ui/Comment';
export * from './model/comments';
// ✅ 좋은 예시
export { Comment } from './ui/comment';
export { CommentForm } from './ui/comment-form';
export { useComments } from './model/use-comments';
export type { Comment, CommentFormData } from './model/types';
```
### 3. 슬라이스 독립성
- **Zero Coupling**: 같은 레이어의 슬라이스들은 서로 독립적이어야 합니다
- **High Cohesion**: 슬라이스는 주요 목표와 관련된 대부분의 코드를 포함해야 합니다
- 예외: Entities 레이어에서 필요한 경우 `@x` 표기법으로 cross-import 가능
### 4. 세그먼트 명명 규칙
세그먼트 이름은 **목적**을 설명해야 하며, **본질**을 설명하면 안 됩니다:
- ❌ **나쁜 이름**: `components`, `hooks`, `types` (무엇인지만 설명)
- ✅ **좋은 이름**: `ui`, `api`, `model`, `lib` (왜 있는지 설명)
## 코드 규칙
- TypeScript 사용 (strict mode)
- 함수형 컴포넌트 사용
- Named exports 선호
- Tailwind CSS 사용
- ESLint + Prettier 적용
- Public API는 명시적으로 정의 (와일드카드 export 지양)
## 아키텍처 린팅
코드베이스의 FSD 규칙 준수를 자동으로 검증하려면 [Steiger](https://github.com/feature-sliced/steiger)를 사용하세요.
```bash
# Steiger 설치 및 실행
pnpm add -D @feature-sliced/steiger
npx steiger src
```
@@ -0,0 +1,31 @@
"use client";
import { useCallback } from "react";
import { usePathname, useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
import { BaseLayout } from "@common/components";
import type { MenuItem } from "@/entities/menu";
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 handleMenuClick = useCallback(
(item: MenuItem) => {
router.push(item.urlPath);
},
[router],
);
return (
<BaseLayout
queryClient={queryClient}
currentPath={pathname ?? ""}
handleMenuClick={handleMenuClick}
>
<PageLayout>{children}</PageLayout>
</BaseLayout>
);
}
@@ -0,0 +1,9 @@
"use client";
export default function MainPage() {
return (
<div className="flex h-full flex-col items-center justify-center py-10">
<h1 className="mb-4 text-3xl font-bold">Home</h1>
</div>
);
}
+169
View File
@@ -0,0 +1,169 @@
# App Layer
**App**은 애플리케이션 전체에 영향을 미치는 기술적, 비즈니스적 사항을 다루는 최상위 레이어입니다.
## 목적
- 애플리케이션의 진입점(entrypoint) 제공
- 전역 Context Providers 설정
- 라우터 구성
- 글로벌 스타일 정의
- 전역 상태 저장소 설정
- 애널리틱스 및 모니터링 설정
## 특징
- **슬라이스 없음**: App 레이어는 Shared처럼 슬라이스가 없고 세그먼트로만 구성됩니다
- **전체 앱 관심사**: 특정 비즈니스 도메인이 아닌 앱 전체와 관련된 코드만 포함
- **최상위 레이어**: 모든 하위 레이어의 모듈을 자유롭게 import 가능
## 구조 (Next.js App Router)
Next.js를 사용하는 경우, App 레이어는 주로 `app/` 디렉토리로 표현됩니다:
```
app/
├── (routes)/ # 라우트 그룹 및 페이지
│ ├── (auth)/
│ │ ├── login/
│ │ │ └── page.tsx
│ │ └── register/
│ │ └── page.tsx
│ └── (main)/
│ ├── dashboard/
│ │ └── page.tsx
│ └── settings/
│ └── page.tsx
├── layout.tsx # 루트 레이아웃
├── page.tsx # 홈 페이지
├── globals.css # 글로벌 스타일
├── providers.tsx # 전역 Providers 설정
└── error.tsx # 글로벌 에러 핸들러
```
### 일반적인 FSD 세그먼트 구조
FSD 표준 세그먼트를 사용하는 경우:
```
app/
├── entrypoint/ # 애플리케이션 진입점 (프레임워크별)
│ └── index.tsx
├── providers/ # Context Providers
│ ├── query-provider.tsx
│ ├── theme-provider.tsx
│ └── index.tsx
├── styles/ # 글로벌 스타일
│ ├── globals.css
│ └── theme.css
├── store/ # 글로벌 상태 저장소 설정
│ └── index.ts
└── routes/ # 라우터 설정
└── index.tsx
```
## 예시
### Next.js Providers 설정
```typescript
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from 'next-themes';
import { useState } from 'react';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
}));
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
</QueryClientProvider>
);
}
// app/layout.tsx
import { Providers } from './providers';
import './globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko">
<body>
<Providers>
{children}
</Providers>
</body>
</html>
);
}
```
### 글로벌 설정
```typescript
// app/config/analytics.ts
export function initAnalytics() {
if (typeof window !== 'undefined') {
// 애널리틱스 초기화
}
}
// app/config/monitoring.ts
export function initMonitoring() {
// 에러 모니터링 설정
}
```
## Next.js App Router와 FSD
Next.js의 `app/` 디렉토리는 FSD의 **App 레이어**와 **Pages 레이어**를 동시에 담당합니다:
- **루트 레벨 파일들** (`layout.tsx`, `providers.tsx`, `globals.css`): App 레이어
- **라우트 폴더들** (`dashboard/`, `login/` 등): Pages 레이어
각 페이지별 비즈니스 로직은:
- 재사용되지 않는 UI는 페이지 슬라이스(`app/dashboard/` 등) 안에 유지
- 재사용되는 기능은 `features/` 레이어로 분리
- 여러 페이지에서 재사용되는 큰 UI 블록은 `widgets/` 레이어로 분리
## 레이어 의존성 규칙
✅ **허용되는 의존성**:
- 모든 하위 레이어 (`pages`, `widgets`, `features`, `entities`, `shared`)
- 같은 `app` 레이어 내의 다른 세그먼트
- 외부 라이브러리
❌ **금지된 의존성**:
- 없음 (App은 최상위 레이어)
## 주의사항
1. **비대화 방지**: App 레이어가 너무 커지면 관리가 어려워집니다. 대부분의 로직은 하위 레이어에 배치하세요
2. **전역 관심사만**: 특정 페이지나 기능에만 관련된 코드는 해당 레이어로 이동
3. **라우터 레벨 로직**: 라우터 및 서버 레벨 로직은 이 레이어에 유지
4. **Processes 레이어 사용 지양**: Processes 레이어는 deprecated되었으므로 해당 로직은 `app`이나 `features`에 배치
## FSD v2.1 권장사항
최신 FSD 버전(v2.1)은 **"pages-first"** 접근 방식을 권장합니다:
- 더 많은 코드를 페이지 슬라이스에 유지
- 큰 UI 블록과 재사용되지 않는 비즈니스 로직은 즉시 추출하지 말고 페이지에 보관
- 실제로 재사용이 필요할 때만 `features`나 `widgets`로 분리
이 접근 방식은 과도한 추상화를 방지하고 코드를 더 쉽게 탐색할 수 있게 합니다.
+15
View File
@@ -0,0 +1,15 @@
@import "@common/assets/styles/base.css";
@import "@common/assets/styles/diveicon.css";
@import "@common/components/index.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body {
color: var(--foreground);
background: var(--background);
font-family: var(--font-noto-sans-kr), sans-serif;
font-size: 10px;
}
+30
View File
@@ -0,0 +1,30 @@
import type { Metadata } from "next";
import { Noto_Sans_KR } from "next/font/google";
import { Providers } from "./providers";
import "./globals.css";
const notoSansKR = Noto_Sans_KR({
subsets: ["latin"],
weight: ["100", "300", "400", "500", "700", "900"],
display: "swap",
variable: "--font-noto-sans-kr",
});
export const metadata: Metadata = {
title: "행정지원시스템",
description: "행정지원시스템",
};
interface RootLayoutProps {
children: React.ReactNode;
}
export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="ko" className={notoSansKR.variable}>
<body className={`${notoSansKR.className} antialiased`}>
<Providers>{children}</Providers>
</body>
</html>
);
}
+18
View File
@@ -0,0 +1,18 @@
"use client";
import { QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { queryClient } from "@/shared/lib";
interface ProvidersProps {
children: React.ReactNode;
}
export function Providers({ children }: ProvidersProps) {
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
+114
View File
@@ -0,0 +1,114 @@
# Entities Layer
**Entities**는 프로젝트가 다루는 실제 비즈니스 개념을 나타내는 레이어입니다. 비즈니스에서 제품을 설명할 때 사용하는 용어들(예: User, Product, Order)이 주로 엔티티가 됩니다.
## 목적
- 비즈니스 엔티티의 데이터 모델 및 타입 정의
- 엔티티 관련 API 요청 함수 관리
- 엔티티의 시각적 표현 제공 (재사용 가능한 UI)
- 여러 페이지에서 동일한 외관을 재사용할 수 있도록 지원
## 구조
각 엔티티 슬라이스는 다음 세그먼트로 구성됩니다:
```
entities/
└── [entity-name]/
├── model/ # 데이터 저장소, 검증 스키마, 상태 관리
├── api/ # 엔티티 관련 API 요청 함수
├── ui/ # 엔티티 시각적 표현 (선택적)
├── lib/ # 엔티티 관련 유틸리티 (선택적)
├── @x/ # 다른 엔티티로부터의 cross-import용 Public API (선택적)
└── index.ts # Public API
```
### 세그먼트 설명
- **model**: 데이터 모델, 타입 정의, 상태 저장소 (Zustand 등)
- **api**: 백엔드 API 호출 함수
- **ui**: 엔티티를 표시하는 UI 컴포넌트 (완전한 UI 블록이 아니라 재사용 가능한 표현)
- **lib**: 엔티티 관련 유틸리티 함수
- **@x**: 다른 엔티티가 이 엔티티를 import할 때 사용하는 명시적 cross-import API
- **index.ts**: 슬라이스의 Public API (외부에서 사용할 수 있는 것만 export)
## 예시
```typescript
// entities/user/model/user.ts
export interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
// entities/user/api/get-user.ts
import type { User } from '../model/user';
export async function getUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// entities/user/ui/user-card.tsx
import type { User } from '../model/user';
interface UserCardProps {
user: User;
onClick?: () => void;
}
export function UserCard({ user, onClick }: UserCardProps) {
return (
<div onClick={onClick}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}
// entities/user/index.ts
export { type User } from './model/user';
export { getUser } from './api/get-user';
export { UserCard } from './ui/user-card';
```
## 엔티티 간 관계
엔티티가 서로 연결되어 있을 때는 `@x` 표기법을 사용하여 명시적으로 cross-import를 관리하는 것이 권장됩니다:
```typescript
// entities/artist/model/artist.ts
import type { Song } from 'entities/song/@x/artist';
export interface Artist {
name: string;
songs: Array<Song>;
}
// entities/song/@x/artist.ts
export type { Song } from '../model/song';
```
> `@x` 폴더를 사용하면 엔티티 간 연결을 명확하게 만들어 함께 리팩터링이 필요한 코드를 쉽게 찾을 수 있습니다.
## 레이어 의존성 규칙
✅ **허용되는 의존성**:
- `shared` 레이어의 모듈
- 같은 엔티티 슬라이스 내의 다른 세그먼트
- 다른 엔티티 슬라이스 (필요한 경우, `@x` 표기법 권장)
❌ **금지된 의존성**:
- `features` 레이어
- `widgets` 레이어
- `pages` 레이어
- `app` 레이어
## 주의사항
1. **UI 컴포넌트는 완전한 비즈니스 로직을 포함하지 않음**: UI는 주로 props나 slots를 통해 다양한 비즈니스 로직을 연결할 수 있도록 설계
2. **엔티티 간 비즈니스 로직은 상위 레이어에**: 엔티티 간 상호작용의 비즈니스 로직은 Features나 Pages 레이어에서 처리
3. **데이터 관리에 집중**: 복잡한 비즈니스 로직보다는 데이터 모델, 검증, 저장에 집중
+1
View File
@@ -0,0 +1 @@
export { type MenuItem } from "./model/types";
@@ -0,0 +1,19 @@
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;
}
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;
+130
View File
@@ -0,0 +1,130 @@
# Features Layer
**Features**는 앱의 주요 사용자 상호작용과 기능을 구현하는 레이어입니다. 사용자가 실제로 수행하는 작업들로, 주로 비즈니스 엔티티와 관련이 있습니다.
## 목적
- 사용자가 중요하게 생각하는 주요 상호작용 구현
- 재사용 가능한 비즈니스 기능 제공
- 여러 페이지에서 사용되는 기능의 중복 제거
## Feature로 만들어야 하는 경우
> ⚠️ **중요**: 모든 것을 feature로 만들 필요는 없습니다.
Feature로 만드는 것이 좋은 지표:
- **여러 페이지에서 재사용됨**: 예를 들어, 여러 에디터가 있고 모두 댓글 기능이 있다면, 댓글은 재사용 가능한 feature입니다
- **중요한 비즈니스 기능**: 신규 프로젝트 참여자가 Features 폴더를 보고 앱의 주요 기능을 빠르게 파악할 수 있어야 합니다
Feature가 너무 많으면 중요한 기능이 묻히므로, 신규 참여자의 경험을 최적화하여 결정하세요.
## 구조
각 feature 슬라이스는 다음 세그먼트로 구성됩니다:
```
features/
└── [feature-name]/
├── ui/ # 상호작용을 수행하는 UI (폼, 버튼 등)
├── api/ # 액션 수행에 필요한 API 호출 (선택적)
├── model/ # 검증 및 내부 상태 관리 (선택적)
├── lib/ # 유틸리티 함수 (선택적)
├── config/ # feature flags (선택적)
└── index.ts # Public API
```
### 세그먼트 설명
- **ui**: 사용자 상호작용을 위한 UI 컴포넌트 (폼, 모달, 버튼 등)
- **api**: feature 실행에 필요한 API 요청 함수
- **model**: 폼 검증, 내부 상태, 비즈니스 로직
- **lib**: feature 내부에서만 사용되는 유틸리티
- **config**: feature flags, feature별 설정값
- **index.ts**: 슬라이스의 Public API (외부에서 사용할 수 있는 것만 export)
## 예시
```typescript
// features/auth-login/ui/login-form.tsx
import { useState } from 'react';
import { loginUser } from '../api/login-user';
import type { LoginCredentials } from '../model/types';
export function LoginForm() {
const [credentials, setCredentials] = useState<LoginCredentials>({
email: '',
password: ''
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await loginUser(credentials);
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={credentials.email}
onChange={(e) => setCredentials({ ...credentials, email: e.target.value })}
/>
<input
type="password"
value={credentials.password}
onChange={(e) => setCredentials({ ...credentials, password: e.target.value })}
/>
<button type="submit">로그인</button>
</form>
);
}
// features/auth-login/api/login-user.ts
import type { LoginCredentials } from '../model/types';
export async function loginUser(credentials: LoginCredentials) {
const response = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify(credentials),
});
return response.json();
}
// features/auth-login/model/types.ts
export interface LoginCredentials {
email: string;
password: string;
}
// features/auth-login/index.ts
export { LoginForm } from './ui/login-form';
export { loginUser } from './api/login-user';
export type { LoginCredentials } from './model/types';
```
## 레이어 의존성 규칙
✅ **허용되는 의존성**:
- `shared` 레이어의 모듈
- `entities` 레이어의 모듈
- 같은 feature 슬라이스 내의 다른 세그먼트
❌ **금지된 의존성**:
- 다른 `features` 슬라이스 (feature 간 직접 의존 금지)
- `widgets` 레이어
- `pages` 레이어
- `app` 레이어
> 💡 **팁**: 여러 features가 공통 기능을 필요로 한다면, 그 기능을 `shared` 레이어로 이동시키거나 `entities` 레이어에서 관리하세요.
## 명명 규칙
- 소문자와 대시 사용: `auth-login`, `post-create`, `comment-edit`
- 동사 형태 권장: 사용자 액션을 나타내는 이름 사용
- 명확하고 구체적으로: 기능의 목적이 명확하게 드러나도록
## 주의사항
1. **하나의 기능에만 집중**: 각 feature는 단일 사용자 시나리오를 구현해야 합니다
2. **재사용성 고려**: 여러 페이지에서 사용될 수 있도록 설계
3. **독립성 유지**: 다른 features에 의존하지 않고 독립적으로 작동
4. **적절한 추상화**: 너무 많은 feature를 만들지 말고, 중요한 기능 위주로 구성
+8
View File
@@ -0,0 +1,8 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactStrictMode: true,
transpilePackages: ["@common/assets"],
};
export default nextConfig;
+36
View File
@@ -0,0 +1,36 @@
{
"name": "as",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint:fsd": "steiger ."
},
"dependencies": {
"@common/assets": "workspace:*",
"@common/components": "workspace:*",
"@common/network": "workspace:*",
"@hookform/resolvers": "^4.1.3",
"@tanstack/react-query": "^5.75.5",
"@tanstack/react-query-devtools": "^5.75.5",
"@tanstack/react-table": "^8.21.2",
"axios": "^1.8.1",
"dayjs": "^1.11.13",
"next": "15.3.8",
"react-hook-form": "^7.54.2",
"zod": "^3.24.2"
},
"devDependencies": {
"eslint": "^9.0.0",
"eslint-config-next": "15.4.0",
"postcss": "^8.4.49",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11",
"steiger": "^0.5.11",
"tailwindcss": "^3.4.14",
"typescript": "^5.7.2"
}
}
+8
View File
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;
+149
View File
@@ -0,0 +1,149 @@
# Shared Layer
**Shared**는 애플리케이션의 기반을 형성하는 레이어입니다. 외부 세계(백엔드, 서드파티 라이브러리, 환경)와의 연결을 생성하고, 높은 응집력을 가진 자체 라이브러리를 정의하는 곳입니다.
## 목적
- 외부 시스템 및 라이브러리와의 통합
- 프로젝트 전체에서 사용되는 공통 코드 제공
- 비즈니스 로직이 없는 재사용 가능한 유틸리티 및 UI 제공
- 글로벌 설정 및 환경 변수 관리
## 특징
- **슬라이스 없음**: Shared 레이어는 비즈니스 도메인이 없으므로 슬라이스로 나뉘지 않습니다
- **세그먼트 구성**: 기술적 목적에 따라 세그먼트로 직접 구성됩니다
- **자유로운 import**: Shared 내의 모든 파일은 서로를 참조하고 import할 수 있습니다
## 구조
```
shared/
├── ui/ # 애플리케이션 UI Kit
│ ├── button/
│ ├── input/
│ ├── modal/
│ └── index.ts
├── api/ # API 클라이언트 및 백엔드 엔드포인트 함수
│ ├── client.ts
│ └── index.ts
├── lib/ # 내부 라이브러리 모음
│ ├── dates/ # 날짜 관련 유틸리티
│ ├── colors/ # 색상 관련 유틸리티
│ ├── text/ # 텍스트 처리 유틸리티
│ └── index.ts
├── config/ # 환경 변수, 글로벌 feature flags
│ ├── env.ts
│ └── index.ts
├── types/ # 공통 타입 정의
│ └── index.ts
├── i18n/ # 번역 설정, 글로벌 번역 문자열
│ └── index.ts
└── routes/ # 라우트 상수 또는 패턴
└── index.ts
```
## 세그먼트 설명
### 📁 ui
애플리케이션의 UI Kit로, 비즈니스 로직을 포함하지 않는 재사용 가능한 UI 컴포넌트들입니다.
- Button, Input, Modal 같은 기본 컴포넌트
- 비즈니스 로직은 없지만 UI 로직은 포함 가능 (예: Autocomplete, SearchBar)
- 비즈니스 테마 컴포넌트도 허용 (예: 회사 로고, 페이지 레이아웃)
### 📁 api
API 클라이언트 및 특정 백엔드 엔드포인트 요청 함수입니다.
- HTTP 클라이언트 설정 (Axios, Fetch 등)
- 인증 인터셉터
- 공통 에러 핸들링
### 📁 lib
내부 라이브러리 모음으로, 각 라이브러리는 **하나의 관심사**에 집중해야 합니다.
> ⚠️ **중요**: `helpers`나 `utils` 폴더처럼 덤프 폴더로 만들지 마세요. 각 라이브러리는 명확한 초점 영역(예: dates, colors, text)을 가져야 하며, README로 문서화해야 합니다.
### 📁 config
환경 변수, 글로벌 feature flags 및 앱 전체 설정입니다.
### 📁 types
프로젝트 전체에서 사용되는 공통 타입 정의입니다.
### 📁 i18n
번역 설정 코드 및 글로벌 번역 문자열입니다.
### 📁 routes
라우트 상수 또는 라우트 매칭 패턴입니다.
## 예시
```typescript
// shared/ui/button/button.tsx
interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary';
onClick?: () => void;
}
export function Button({ children, variant = 'primary', onClick }: ButtonProps) {
return (
<button
className={variant === 'primary' ? 'bg-blue-500' : 'bg-gray-500'}
onClick={onClick}
>
{children}
</button>
);
}
// shared/api/client.ts
import axios from 'axios';
export const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
timeout: 10000,
});
// shared/lib/dates/format-date.ts
import dayjs from 'dayjs';
export function formatDate(date: Date, format: string = 'YYYY-MM-DD'): string {
return dayjs(date).format(format);
}
// shared/config/env.ts
export const ENV = {
apiUrl: process.env.NEXT_PUBLIC_API_URL!,
isDev: process.env.NODE_ENV === 'development',
} as const;
```
## 레이어 의존성 규칙
✅ **허용되는 의존성**:
- 같은 `shared` 레이어 내의 다른 세그먼트 (자유로운 cross-import)
- 외부 라이브러리 및 패키지
❌ **금지된 의존성**:
- `entities` 레이어
- `features` 레이어
- `widgets` 레이어
- `pages` 레이어
- `app` 레이어
> 💡 **원칙**: Shared는 가장 낮은 레이어이므로 상위 레이어에 의존하지 않습니다.
## 세그먼트 명명 규칙
세그먼트 이름은 **내용의 목적**을 설명해야 하며, **본질**을 설명하면 안 됩니다.
❌ **나쁜 예시**: `components`, `hooks`, `types` (본질을 설명)
✅ **좋은 예시**: `ui`, `api`, `lib/dates`, `config` (목적을 설명)
## 주의사항
1. **비즈니스 로직 없음**: Shared는 순수한 기술적 코드만 포함
2. **높은 재사용성**: 앱 전체에서 사용될 수 있어야 함
3. **명확한 라이브러리 구조**: `lib` 내부는 관심사별로 명확하게 분리
4. **문서화**: 각 라이브러리는 README로 사용법과 목적을 문서화
+9
View File
@@ -0,0 +1,9 @@
import { Client } from "@common/network";
import { PROXY_HOST, SITE_ID } from "@/shared/config/env";
const BASE_URL = `${PROXY_HOST}/server`;
export const apiClient = Client.getInstance({
baseURL: BASE_URL,
siteId: SITE_ID,
});
+1
View File
@@ -0,0 +1 @@
export { apiClient } from "./client";
+11
View File
@@ -0,0 +1,11 @@
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 || "AS";
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";
+1
View File
@@ -0,0 +1 @@
export * from "./env";
+12
View File
@@ -0,0 +1,12 @@
import dayjs from "dayjs";
import "dayjs/locale/ko";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(relativeTime);
dayjs.locale("ko");
export { dayjs };
+2
View File
@@ -0,0 +1,2 @@
export { queryClient } from "./query-client";
export { dayjs } from "./dayjs";
+11
View File
@@ -0,0 +1,11 @@
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1,
refetchOnWindowFocus: false,
staleTime: 60 * 1000,
},
},
});
+29
View File
@@ -0,0 +1,29 @@
export interface BaseEntity {
id: string;
createdAt: string;
updatedAt: string;
}
export interface PaginationParams {
page: number;
limit: number;
}
export interface PaginationResponse<T> {
data: T[];
total: number;
page: number;
limit: number;
}
export interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
export interface ApiError {
message: string;
code?: string;
status?: number;
}
+1
View File
@@ -0,0 +1 @@
export { PageLayout } from "./page-layout";
+35
View File
@@ -0,0 +1,35 @@
"use client";
import { PageTitle } from "@common/components";
const mockCurrentMenuInfo = {
menuCd: 0,
menuKornNm: "메인",
menuEngNm: "Main",
parentId: null,
sortSeq: 0,
children: null,
levelNo: 0,
urlPath: "/",
menuLnkgTypeCd: "",
menuLnkgSn: 0,
menuPath: "메인",
qrcd: "",
useYn: "Y",
otptYn: "Y",
newWndwYn: "N",
requiredUrlPath: false,
};
function PageLayout({ children }: { children: React.ReactNode }) {
const menuPaths = mockCurrentMenuInfo.menuPath.split(">").map((path) => path.trim());
return (
<div className="bg-container h-auto min-h-full pb-2">
<PageTitle title={mockCurrentMenuInfo.menuKornNm} menuPaths={menuPaths} />
{children}
</div>
);
}
export { PageLayout };
+29
View File
@@ -0,0 +1,29 @@
import type { Config } from "tailwindcss";
import presetConfig from "../../tailwind.config.preset";
const config: Config = {
presets: [presetConfig],
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./shared/**/*.{js,ts,jsx,tsx,mdx}",
"./entities/**/*.{js,ts,jsx,tsx,mdx}",
"./features/**/*.{js,ts,jsx,tsx,mdx}",
"./widgets/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"../../packages/common/components/dist/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
},
fontFamily: {
sans: ["var(--font-noto-sans-kr)", "Noto Sans KR", "sans-serif"],
},
},
},
plugins: [],
};
export default config;
+35
View File
@@ -0,0 +1,35 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"],
"@/app/*": ["./app/*"],
"@/shared/*": ["./shared/*"],
"@/entities/*": ["./entities/*"],
"@/features/*": ["./features/*"],
"@/widgets/*": ["./widgets/*"],
"@/pages/*": ["./pages/*"],
"@common/components": ["../../packages/common/components/dist/"],
"@common/network": ["../../packages/common/network/"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
+135
View File
@@ -0,0 +1,135 @@
# Widgets Layer
**Widgets**는 큰 규모의 독립적인 UI 블록을 위한 레이어입니다. 여러 features와 entities를 조합하여 완전한 기능을 제공하는 복합 컴포넌트를 담습니다.
## 목적
- 큰 규모의 독립적인 UI 블록 구성
- 여러 features와 entities를 하나의 응집력 있는 UI로 결합
- 여러 페이지에서 재사용되는 복잡한 UI 제공
- 페이지 레벨 레이아웃 및 섹션 관리
## Widget으로 만들어야 하는 경우
Widget은 다음 조건에 해당할 때 사용하세요:
✅ **Widget으로 만들기 좋은 경우**:
- 여러 페이지에서 재사용되는 큰 UI 블록
- 페이지에 여러 개의 큰 독립적 블록이 있고, 이것이 그 중 하나인 경우
- 페이지 레벨 레이아웃 컴포넌트 (nested routing 시스템 사용 시)
❌ **Widget으로 만들지 말아야 하는 경우**:
- UI 블록이 페이지의 대부분을 차지하고 재사용되지 않는 경우 → 페이지 슬라이스 내부에 직접 배치
- 작은 재사용 컴포넌트 → `shared/ui`에 배치
## 구조
각 widget 슬라이스는 다음 세그먼트로 구성됩니다:
```
widgets/
└── [widget-name]/
├── ui/ # 위젯의 메인 UI 컴포넌트
├── model/ # 위젯 내부 상태 관리 (선택적)
├── api/ # 위젯 전용 API 호출 (선택적)
├── lib/ # 위젯 내부 유틸리티 (선택적)
└── index.ts # Public API
```
### 세그먼트 설명
- **ui**: 여러 features와 entities를 조합한 위젯 UI
- **model**: 위젯 내부 상태 및 로직 (선택적)
- **api**: 위젯이 필요로 하는 데이터 fetching (선택적)
- **lib**: 위젯 내부에서만 사용되는 헬퍼 함수 (선택적)
- **index.ts**: 슬라이스의 Public API (외부에서 사용할 수 있는 것만 export)
## 예시
```typescript
// widgets/header/ui/header.tsx
import { Logo } from 'shared/ui/logo';
import { Navigation } from 'shared/ui/navigation';
import { UserMenu } from 'features/user-menu';
import { NotificationBell } from 'features/notifications';
export function Header() {
return (
<header>
<Logo />
<Navigation />
<NotificationBell />
<UserMenu />
</header>
);
}
// widgets/product-list/ui/product-list.tsx
import { useProducts } from '../api/use-products';
import { ProductCard } from 'entities/product';
import { AddToCartButton } from 'features/cart-add-item';
export function ProductList() {
const { data: products, isLoading } = useProducts();
if (isLoading) return <div>로딩 중...</div>;
return (
<div>
{products.map((product) => (
<div key={product.id}>
<ProductCard product={product} />
<AddToCartButton productId={product.id} />
</div>
))}
</div>
);
}
// widgets/product-list/api/use-products.ts
import { useQuery } from '@tanstack/react-query';
import { getProducts } from 'entities/product';
export function useProducts() {
return useQuery({
queryKey: ['products'],
queryFn: getProducts,
});
}
// widgets/product-list/index.ts
export { ProductList } from './ui/product-list';
```
## 레이어 의존성 규칙
✅ **허용되는 의존성**:
- `shared` 레이어의 모듈
- `entities` 레이어의 모듈
- `features` 레이어의 모듈
- 같은 widget 슬라이스 내의 다른 세그먼트
❌ **금지된 의존성**:
- 다른 `widgets` 슬라이스 (widget 간 직접 의존 금지)
- `pages` 레이어
- `app` 레이어
## 명명 규칙
- 소문자와 대시 사용: `header`, `sidebar`, `product-list`
- 명사 형태 권장: UI 블록의 성격을 나타내는 이름 사용
- 구체적으로: 위젯의 역할이 명확하게 드러나도록
## Nested Routing 사용 시
Remix 같은 nested routing 시스템을 사용한다면:
- Widgets 레이어를 flat routing의 Pages 레이어처럼 사용 가능
- 완전한 라우터 블록 생성 (데이터 fetching, loading states, error boundaries 포함)
- 페이지 레이아웃을 이 레이어에 저장 가능
## 주의사항
1. **독립성 유지**: 각 위젯은 독립적으로 작동 가능해야 합니다
2. **적절한 크기**: 너무 작은 컴포넌트는 `shared/ui`에, 재사용되지 않으면 `pages`에 배치
3. **응집력**: 관련된 features와 entities를 의미 있게 조합
4. **재사용 최적화**: 여러 페이지에서 동일한 방식으로 사용될 수 있도록 설계
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+13 -6
View File
@@ -1,7 +1,14 @@
# apps/cs/src/shared/config/env.ts 참고
NEXT_PUBLIC_PORT=3000
NEXT_PUBLIC_PROXY_HOST=http://localhost:3000
NEXT_PUBLIC_SITE_ID=CS
NEXT_PUBLIC_SUB_DOMAIN=http://localhost:3000
NEXT_PUBLIC_SERVER_API_URL=http://192.168.0.102:8000
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
NEXT_PUBLIC_PORT=3001
NEXT_PUBLIC_PROXY_HOST=http://localhost:3001
NEXT_PUBLIC_SITE_ID=CM
NEXT_PUBLIC_SUB_DOMAIN=http://localhost:13000
# NEXT_PUBLIC_SERVER_API_URL=http://localhost:8000
NEXT_PUBLIC_SERVER_API_URL=http://dev.onasys.co.kr:8000
NEXT_PUBLIC_LINK_BACKEND_API_HOST=http://rsone.iptime.org:10014
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
NEXT_PUBLIC_SERVER_API_URL2=http://con.mind-one.co.kr:11083
NEXT_PUBLIC_SERVER_API_URL3=https://apigateway.apps.rsonesoft.com
NEXT_PUBLIC_SN=https://devlion.hasuon.co.kr
NEXT_PUBLIC_WR=https://devtiger.hasuon.co.kr
-10
View File
@@ -1,10 +0,0 @@
# apps/cs/src/shared/config/env.ts 참고
NEXT_PUBLIC_PORT=3000
NEXT_PUBLIC_PROXY_HOST=http://localhost:3000
NEXT_PUBLIC_SITE_ID=CS
NEXT_PUBLIC_SUB_DOMAIN=http://localhost:13000
NEXT_PUBLIC_SERVER_API_URL=http://dev.onasys.co.kr:8000
NEXT_PUBLIC_LINK_BACKEND_API_HOST=http://rsone.iptime.org:10014
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
NEXT_PUBLIC_SERVER_API_URL2=http://con.mind-one.co.kr:11083/
-7
View File
@@ -1,7 +0,0 @@
# apps/cs/src/shared/config/env.ts 참고
NEXT_PUBLIC_PORT=3000
NEXT_PUBLIC_PROXY_HOST=http://localhost:3000
NEXT_PUBLIC_SITE_ID=CS
NEXT_PUBLIC_SUB_DOMAIN=http://localhost:3000
NEXT_PUBLIC_SERVER_API_URL=http://192.168.0.102:8000
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
BIN
View File
Binary file not shown.
+2
View File
@@ -10,6 +10,8 @@
!.yarn/releases
!.yarn/versions
pnpm_lock.yaml
# testing
/coverage
+28 -12
View File
@@ -36,9 +36,15 @@ async function handler(
}
try {
if (!isRefreshHandler) {
body = await request.json();
console.log(`[Proxy] Request body:`, body);
// GET 요청이 아닐 때만 body 파싱
if (method !== "GET" && !isRefreshHandler) {
try {
body = await request.json();
console.log(`[Proxy] Request body:`, body);
} catch {
// body가 없을 수 있음
body = {};
}
}
const targetUrl = `${SERVER_API_URL}/${params.url.join("/")}`;
@@ -47,7 +53,7 @@ async function handler(
const result = await fetch(targetUrl, {
method,
headers,
body: JSON.stringify(body),
body: method !== "GET" ? JSON.stringify(body) : undefined,
});
console.log(`[Proxy] Response status: ${result.status}`);
@@ -55,12 +61,14 @@ async function handler(
// 2FA 관련 엔드포인트인 경우 응답 본문도 확인
const is2FAEndpoint = params.url.includes("2fa") || params.url.includes("auth");
// 배너 API는 인증 없이 호출 가능
const isBannerEndpoint = params.url.includes("bnrs");
let responseData = {};
if (is2FAEndpoint) {
if (is2FAEndpoint || isBannerEndpoint) {
try {
responseData = await result.json();
console.log(`[Proxy] 2FA Response data:`, responseData);
console.log(`[Proxy] ${is2FAEndpoint ? '2FA' : 'Banner'} Response data:`, responseData);
} catch {
// JSON 파싱 실패 시 빈 객체 사용
console.log(`[Proxy] Failed to parse JSON response`);
@@ -74,15 +82,15 @@ async function handler(
console.log(`[Proxy] Tokens - accessToken: ${!!accessToken}, userId: ${!!userId}, refreshToken: ${!!refreshToken}`);
console.log(`[Proxy] Available headers:`, Array.from(result.headers.keys()));
// 2FA 엔드포인트이고 실패한 경우 실제 오류 메시지 반환
if (!result.ok && is2FAEndpoint) {
console.log(`[Proxy] 2FA endpoint failed, returning actual error`);
// 2FA 또는 배너 엔드포인트이고 실패한 경우 실제 오류 메시지 반환
if (!result.ok && (is2FAEndpoint || isBannerEndpoint)) {
console.log(`[Proxy] ${is2FAEndpoint ? '2FA' : 'Banner'} endpoint failed, returning actual error`);
return NextResponse.json(responseData, { status: result.status });
}
// 2FA 엔드포인트이고 성공한 경우 (토큰이 헤더에 없을 수 있음)
if (result.ok && is2FAEndpoint) {
console.log(`[Proxy] 2FA endpoint succeeded, returning response data`);
// 2FA 또는 배너 엔드포인트이고 성공한 경우 (토큰이 헤더에 없을 수 있음)
if (result.ok && (is2FAEndpoint || isBannerEndpoint)) {
console.log(`[Proxy] ${is2FAEndpoint ? '2FA' : 'Banner'} endpoint succeeded, returning response data`);
return NextResponse.json(responseData, { status: result.status });
}
@@ -143,3 +151,11 @@ export async function PUT(
const params = await props.params;
return handler(request, params, "PUT");
}
export async function GET(
request: Request,
props: { params: Promise<{ url: string[] }> },
) {
const params = await props.params;
return handler(request, params, "GET");
}
+5
View File
@@ -12,6 +12,11 @@ const compat = new FlatCompat({
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
languageOptions: {
parserOptions: {
tsconfigRootDir: __dirname,
},
},
rules: {
"react-hooks/exhaustive-deps": "off",
},
+39 -27
View File
@@ -17,8 +17,8 @@ const serverApiUrl =
const serverApiUrl3 =
process.env.NEXT_PUBLIC_SERVER_API_URL3 ?? serverApiUrl;
const serverApiUrl4 =
process.env.NEXT_PUBLIC_SERVER_API_URL4 ?? serverApiUrl;
// const serverApiUrl4 =
// process.env.NEXT_PUBLIC_SERVER_API_URL4 ?? serverApiUrl;
const serverApiUrl5 =
process.env.NEXT_PUBLIC_SERVER_API_URL5 ?? serverApiUrl;
@@ -33,19 +33,6 @@ export function middleware(req: NextRequest) {
`[Request]\n Original Pathname: ${pathname}`,
];
if (pathname.startsWith("/server")) {
const queryParams = searchParams.toString();
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
queryParams ? `?${queryParams}` : ""
}`;
logMessages.push(` Rewritten URL: ${newUrl}`);
logMessages.push("\n--------------------------------\n");
console.log(logMessages.join("\n"));
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;
@@ -57,7 +44,7 @@ export function middleware(req: NextRequest) {
logMessages.push(` Rewritten URL: ${newUrl}`);
logMessages.push("\n--------------------------------\n");
console.log(logMessages.join("\n"));
// console.log(logMessages.join("\n"));
return NextResponse.rewrite(new URL(newUrl), {
headers: requestHeaders,
@@ -65,11 +52,13 @@ export function middleware(req: NextRequest) {
}
if (pathname.startsWith("/sewerage-service")) {
if (pathname.startsWith("/server/sewerage-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 = `${serverApiUrl2}${pathname}`;
// const decodedQuery = toDecodedQueryString(searchParams);
const newUrl = `${serverApiUrl2}${pathname.replace("/server/", "/")}${searchParams ? `?${searchParams}` : ""}`;
if (token) requestHeaders.set(CLAIM_NAME, token);
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
@@ -87,7 +76,7 @@ export function middleware(req: NextRequest) {
const requestHeaders = new Headers(req.headers);
const token = req.cookies.get(CLAIM_NAME)?.value;
const userId = req.cookies.get(AUTH_USER_ID)?.value;
const newUrl = `${serverApiUrl3}${pathname}`;
const newUrl = `${serverApiUrl2}${pathname}`;
if (token) requestHeaders.set(CLAIM_NAME, token);
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
@@ -101,11 +90,12 @@ export function middleware(req: NextRequest) {
});
}
if (pathname.startsWith("/operation-service")) {
if (pathname.startsWith("/server/operation-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 = `${serverApiUrl4}${pathname}`;
// const newUrl = `${serverApiUrl2}${pathname.replace("/server/", "")}`;
const newUrl = `${serverApiUrl2}${pathname.replace("/server/", "/")}${searchParams ? `?${searchParams}` : ""}`;
if (token) requestHeaders.set(CLAIM_NAME, token);
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
@@ -119,11 +109,12 @@ export function middleware(req: NextRequest) {
});
}
if (pathname.startsWith("/ic-service")) {
if (pathname.startsWith("/server/ic-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 = `${serverApiUrl5}${pathname}`;
const newUrl = `${serverApiUrl3}${pathname.replace("/server/", "/")}`;
if (token) requestHeaders.set(CLAIM_NAME, token);
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
@@ -137,11 +128,12 @@ export function middleware(req: NextRequest) {
});
}
if (pathname.startsWith("/dc-service")) {
if (pathname.startsWith("/server/dc-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 = `${serverApiUrl5}${pathname}`;
const newUrl = `${serverApiUrl3}${pathname.replace("/server/", "/")}${searchParams ? `?${searchParams}` : ""}`;
if (token) requestHeaders.set(CLAIM_NAME, token);
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
@@ -163,11 +155,25 @@ export function middleware(req: NextRequest) {
logMessages.push(` User Service Rewritten URL: ${newUrl}`);
logMessages.push("\n--------------------------------\n");
console.log(logMessages.join("\n"));
// console.log(logMessages.join("\n"));
return NextResponse.rewrite(new URL(newUrl));
}
if (pathname.startsWith("/server")) {
const queryParams = searchParams.toString();
const newUrl = `${serverApiUrl}${pathname.replace("/server", "/")}${
queryParams ? `?${queryParams}` : ""
}`;
logMessages.push(` Rewritten URL: ${newUrl}`);
logMessages.push("\n--------------------------------\n");
// console.log(logMessages.join("\n"));
return NextResponse.rewrite(new URL(newUrl));
}
if (pathname === "/") {
return NextResponse.redirect(new URL("/dashboard", req.url));
}
@@ -178,5 +184,11 @@ export function middleware(req: NextRequest) {
}
export const config = {
matcher: ["/server/:path*", "/attach-service/:path*", "/user-service/:path*", "/"],
matcher: ["/server/:path*",
"/attach-service/:path*",
"/user-service/:path*",
"/sewerage-service/:path*",
"/reuse-service/:path*",
"/operation-service/:path*",
"/"],
};
+14 -6
View File
@@ -3,10 +3,22 @@ import type { NextConfig } from "next";
const subDomain =
process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13000";
const backendUrl = process.env.NEXT_PUBLIC_SERVER_API_URL || "http://192.168.0.102:8000";
const nextConfig: NextConfig = {
output: "standalone",
experimental: {
proxyTimeout: 600000,
},
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
logging: {
fetches: {
fullUrl: true,
},
},
async rewrites() {
return {
beforeFiles: [
@@ -27,10 +39,6 @@ const nextConfig: NextConfig = {
source: "/cms/:path*",
destination: `${subDomain}/cms/:path*`,
},
{
source: "/user-service/:path*",
destination: `${backendUrl}/user-service/:path*`,
},
],
fallback: [],
};
+10 -10
View File
@@ -25,16 +25,16 @@
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@tanstack/react-query": "^5.67.1",
"@tanstack/react-query": "^5.75.5",
"@tanstack/react-query-devtools": "^5.75.5",
"@tanstack/react-table": "^8.21.2",
"@tinymce/tinymce-react": "^6.1.0",
"@tinymce/tinymce-react": "^5.1.1",
"axios": "^1.8.1",
"chart.js": "^4.4.8",
"dayjs": "^1.11.13",
"js-cookie": "^3.0.5",
"lucide-react": "^0.474.0",
"next": "15.3.0",
"next": "15.3.8",
"next-qrcode": "^2.5.1",
"next-themes": "^0.4.6",
"react-chartjs-2": "^5.3.0",
@@ -47,17 +47,17 @@
"zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@eslint/eslintrc": "^3.0.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",
"eslint": "^9.0.0",
"eslint-config-next": "15.4.0",
"postcss": "^8.4.49",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11",
"steiger": "^0.5.5",
"tailwindcss": "^3.4.1",
"typescript": "^5"
"tailwindcss": "^3.4.14",
"typescript": "^5.7.2"
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

+317
View File
@@ -73,12 +73,32 @@
padding: 0 !important;
}
.card_paddingnone {
padding: 0 !important;
}
.card_nomargin {
width: 100%;
margin: 0 !important;
padding: 0 !important;
}
.card_title {
padding: 1rem;
margin-bottom: 1rem;
}
.card_title .leftArea {
display: flex;
align-items: center;
}
.card_title h4 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
}
.topline {
border-top: 1px solid #ccc;
}
@@ -127,3 +147,300 @@
border-bottom-color: #3f4042;
}
}
/* 자료현황 스타일 */
.mid_info_area {
padding: 10px 5px;
height: 220px;
position: relative;
margin-top: 0;
}
.mi_list_swiper {
height: 100%;
width: 100%;
}
.mi_list {
height: 100%;
}
.ml_box {
position: relative;
width: 90%;
max-width: 90%;
height: 150px;
padding: 12px;
margin: 0 8px;
display: flex;
border-radius: 12px;
background: #f8f9fa;
border: 1px solid #e9ecef;
box-sizing: border-box;
transition: all 0.3s ease;
}
.ml_box:hover {
background: #e3f2fd;
border-color: #2196f3;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.mb_section {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
}
.tit {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.tit i {
font-size: 20px;
margin-right: 10px;
color: #2196f3;
}
.tt {
font-size: 16px;
font-weight: 600;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.info_row {
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
}
.info_item {
text-align: center;
width: 100%;
}
.info_label {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.info_value {
font-size: 18px;
font-weight: bold;
color: #2196f3;
line-height: 1;
}
/* Swiper 네비게이션 스타일 */
.mi_list_swiper .swiper-button-next,
.mi_list_swiper .swiper-button-prev {
color: #2196f3;
width: 30px;
height: 30px;
margin-top: -15px;
}
.mi_list_swiper .swiper-button-next:after,
.mi_list_swiper .swiper-button-prev:after {
font-size: 16px;
}
.mi_list_swiper .swiper-pagination {
bottom: 5px;
}
.mi_list_swiper .swiper-pagination-bullet {
background: #2196f3;
opacity: 0.3;
}
.mi_list_swiper .swiper-pagination-bullet-active {
opacity: 1;
}
/* 특별시 스타일 */
.ml_box.special-city {
background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
border-color: #ff4757;
color: white;
}
.ml_box.special-city .tt {
color: white;
font-weight: 700;
}
.ml_box.special-city .info_label {
color: rgba(255, 255, 255, 0.8);
}
.ml_box.special-city .info_value {
color: white;
}
.ml_box.special-city:hover {
background: linear-gradient(135deg, #ff5252, #ff7979);
transform: translateY(-3px);
box-shadow: 0 6px 12px rgba(255, 107, 107, 0.3);
}
/* 광역시 스타일 */
.ml_box.metropolitan-city {
background: linear-gradient(135deg, #4facfe, #00f2fe);
border-color: #2196f3;
color: white;
}
.ml_box.metropolitan-city .tt {
color: white;
font-weight: 600;
}
.ml_box.metropolitan-city .info_label {
color: rgba(255, 255, 255, 0.8);
}
.ml_box.metropolitan-city .info_value {
color: white;
}
.ml_box.metropolitan-city:hover {
background: linear-gradient(135deg, #2196f3, #21cbf3);
transform: translateY(-3px);
box-shadow: 0 6px 12px rgba(33, 150, 243, 0.3);
}
/* 광역도 스타일 */
.ml_box.province {
background: linear-gradient(135deg, #a8edea, #fed6e3);
border-color: #00bcd4;
color: #333;
}
.ml_box.province .tt {
color: #333;
font-weight: 600;
}
.ml_box.province .info_label {
color: #666;
}
.ml_box.province .info_value {
color: #00bcd4;
}
.ml_box.province:hover {
background: linear-gradient(135deg, #00bcd4, #26c6da);
color: white;
transform: translateY(-3px);
box-shadow: 0 6px 12px rgba(0, 188, 212, 0.3);
}
.ml_box.province:hover .tt,
.ml_box.province:hover .info_label,
.ml_box.province:hover .info_value {
color: white;
}
/* 네이버 스타일 로그인 박스 */
.login_info_box {
padding: 20px;
background: #fff;
border: 1px solid #e1e5e9;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.login_header {
display: flex;
align-items: center;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px solid #f0f0f0;
width: 100%;
}
.login_avatar {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
margin-right: 12px;
border: 2px solid #e1e5e9;
}
.login_avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.login_user_info {
flex: 1;
}
.user_name {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 2px;
}
.user_status {
font-size: 12px;
color: #03c75a;
font-weight: 500;
}
.login_actions {
width: 100%;
display: flex;
gap: 8px;
}
.login_btn {
flex: 1;
height: 40px;
background: #03c75a;
color: #fff;
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.login_btn:hover {
background: #02b351;
transform: translateY(-1px);
}
.signup_btn {
flex: 1;
height: 40px;
background: #fff;
color: #03c75a;
border: 1px solid #03c75a;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.signup_btn:hover {
background: #03c75a;
color: #fff;
transform: translateY(-1px);
}
+27 -2
View File
@@ -1,4 +1,4 @@
import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config";
import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, REFRESH_TOKEN } from "@/shared/config";
import { getCookie, setCookie, removeCookie } from "@/shared/lib";
export class TokenManager {
@@ -19,14 +19,37 @@ export class TokenManager {
}
public setToken(key: string, value: string): void {
// 환경에 따른 도메인 설정
const domain = this.getCookieDomain();
setCookie(key, value, {
secure: window.location.protocol === "https:",
sameSite: "lax",
domain: domain,
path: "/", // 모든 경로에서 접근 가능
});
}
private getCookieDomain(): string | undefined {
const hostname = window.location.hostname;
// 개발 환경에서는 localhost 도메인 사용
if (hostname === "localhost" || hostname === "127.0.0.1") {
return undefined; // localhost에서는 도메인 설정하지 않음
}
// 운영 환경에서는 서브도메인을 포함한 최상위 도메인 사용
if (hostname.includes("hasuon.or.kr")) {
return ".hasuon.or.kr";
}
// 기타 환경에서는 현재 호스트명 사용
return hostname;
}
public removeToken(key: string): void {
removeCookie(key);
const domain = this.getCookieDomain();
removeCookie(key, { domain, path: "/" });
}
public setupRefreshTimer(
@@ -53,6 +76,8 @@ export class TokenManager {
this.removeToken(CLAIM_NAME);
this.removeToken(AUTH_USER_ID);
this.removeToken(REFRESH_TOKEN);
this.removeToken(ACCESS_TOKEN);
}
}
+14 -1
View File
@@ -11,9 +11,11 @@ import {
type MyPostList,
type PostParams,
type MyPostParams,
type NoticePostParams,
type PostFormData,
type PostUpdateFormData,
type PostService,
type NoticePostList,
} from "../model/post-interface";
export class PostApiService implements PostService {
@@ -28,11 +30,22 @@ export class PostApiService implements PostService {
return response.data;
}
public async getNoticePostList(
params?: NoticePostParams,
): Promise<NoticePostList> {
const response = await client.get<NoticePostList>(
`${ENDPOINT.BOARD_SERVICE.POSTS}/recentPosts`,
{ params },
);
return response.data;
}
public async getMyPostList(
params?: MyPostParams,
): Promise<MyPostList> {
const response = await client.get<MyPostList>(
`${ENDPOINT.BOARD_SERVICE.POSTS}/mypost`,
`${ENDPOINT.BOARD_SERVICE.MY_POSTS}`,
{ params },
);
return response.data;
+2
View File
@@ -4,11 +4,13 @@ export {
postUpdateFormSchema,
type Post,
type MyPost,
type NoticePost,
type MovePost,
type DeletePost,
type PostList,
type PostParams,
type MyPostParams,
type NoticePostParams,
type PostFormData,
type PostUpdateFormData,
type PostService,
@@ -12,6 +12,20 @@ export interface PostParams extends PageableParams {
ansUseYn?: string;
}
export interface NoticePostParams extends PageableParams {
keywordType?: string;
keyword?: string;
bbsId?: number | null;
delYn?: string;
frDateYmd?: string;
toDateYmd?: string;
}
export interface NoticePost extends Post {
bbsNm: string;
bbsTypeCd: string;
}
export interface MyPostParams extends PageableParams {
keywordType?: string;
keyword?: string;
@@ -99,6 +113,7 @@ export const postUpdateFormSchema = postFormSchema.omit({
export type PostList = PageableList<Post>;
export type MyPostList = PageableList<MyPost>;
export type NoticePostList = PageableList<NoticePost>;
export type PostFormData = z.infer<typeof postFormSchema>;
export type PostUpdateFormData = z.infer<typeof postUpdateFormSchema>;
@@ -0,0 +1,54 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
export interface Banner {
bnrSn: number;
bnrTypeCd: string;
bnrTypeNm: string;
bnrTtl: string;
lnkgUrlAddr: string;
useYn: string;
atflCd: string;
mdfcnDt: string;
mdfr: string;
mdfrNm: string;
}
export const getBannerList = async (siteId: string) => {
const response = await client.get<Banner[]>(
`${ENDPOINT.CMS_SERVICE.BANNERS}/${siteId}`,
);
return response.data;
};
export const getImageUrlByAtflCd = async (atflCd: string): Promise<string> => {
try {
const response = await client.get<Blob>(
`${ENDPOINT.ATTACH_SERVICE.IMAGES}/atflCd/${atflCd}`,
{
responseType: "blob",
},
);
const blob = response.data;
const url = URL.createObjectURL(blob);
return url;
} catch (error: unknown) {
const err = error as
| { status?: number; message?: string; response?: { status?: number } }
| Error
| undefined;
const status = err && typeof err === "object" && "status" in err
? err.status
: err && typeof err === "object" && "response" in err && err.response
? err.response.status
: undefined;
const message = err instanceof Error
? err.message
: err && typeof err === "object" && "message" in err
? String(err.message)
: String(error);
return "";
}
};
@@ -1,14 +0,0 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { Dashboard, DashboardService } from "../model/dashboard-interface";
export class DashboardApiService implements DashboardService {
public async getDashboard(): Promise<Dashboard> {
const response = await client.get<Dashboard>(
ENDPOINT.CMS_SERVICE.DASHBOARD,
);
return response.data;
}
}
export const DashboardApi = new DashboardApiService();
@@ -0,0 +1,48 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageableList } from "@/shared/model";
export interface DataStatus {
ctpvgnsvycnt: number;
filecnt: number;
ctpvnm: string;
}
export const getDataStatusList = async (): Promise<PageableList<DataStatus>> => {
const response = await client.get<DataStatus[]>(
`${ENDPOINT.DC_SERVICE.DATA_STATUS}`,
);
const data = response.data;
console.log("getDataStatusList data:", data);
return {
content: data as DataStatus[],
pageable: {
sort: {
sorted: false,
unsorted: true,
empty: true,
},
offset: 0,
pageNumber: 0,
pageSize: data.length as number,
paged: false,
unpaged: true,
},
sort: {
sorted: false,
unsorted: true,
empty: true,
},
totalPages: 1,
totalElements: data.length,
number: 0,
numberOfElements: data.length,
size: data.length,
first: true,
last: true,
empty: data.length === 0,
};
};
@@ -0,0 +1,376 @@
// apps/cm/src/pages/dashboard/api/fo-service.ts
import { client } from "@/shared/api";
import { createStore } from "zustand/vanilla";
import { create } from "zustand";
import { ENDPOINT } from "@/shared/config";
import type { OperWqltContent, OperFcltContent, ReuseStatusContent, OperationService
} from "../model/fo-interface";
import type { mapOperFcltItem, mapOperFcltType } from "../model/fo-interface";
import type { ReuseMapContent, mapReuseMapType } from "../model/fo-interface";
// 기준년도 조회
export type YearState = {
year: string | null;
};
export type YearStateActions = {
setYear: (year: string | null) => void;
};
export type YearStore = YearState & YearStateActions;
export const defaultYearInitState: YearState = {
year: null,
};
export const createYearStore = (initState: YearState = defaultYearInitState) =>
createStore<YearStore>()((set) => ({
...initState,
setYear: (year) => set(() => ({ year })),
}));
export const yearStore = createYearStore();
// 유역
export type BasinState = {
selectedBasin: string;
};
export type BasinStateActions = {
setSelectedBasin: (selectedBasin: string ) => void;
};
export type BasinStore = BasinState & BasinStateActions;
export const defaultBasinInitState: BasinState = {
selectedBasin: "한강",
};
export const createBasinStore = (initState: BasinState = defaultBasinInitState) =>
createStore<BasinStore>()((set) => ({
...initState,
setSelectedBasin: (selectedBasin) => set(() => ({ selectedBasin })),
}));
export const basinStore = createBasinStore();
//수계별 시설운영 개소수 및 가동률
export type OperFcltState = {
operFcltStatus: OperFcltContent[] | null;
};
export type OperFcltStateActions = {
setOperFcltStatus: (operFcltStatus: OperFcltContent[] | null) => void;
};
export type OperFcltStore = OperFcltState & OperFcltStateActions;
// const initialOperFclt: OperFcltContent[]= [
// {
// "cnt": 710,
// "fctCpc": 2911609.0,
// "rvbsn": "금강",
// "gubun": "R"
// },
// {
// "cnt": 1032,
// "fctCpc": 7506047.0,
// "rvbsn": "낙동강",
// "gubun": "R"
// },
// {
// "cnt": 1261,
// "fctCpc": 2061610.0,
// "rvbsn": "영산강",
// "gubun": "R"
// },
// {
// "cnt": 983,
// "fctCpc": 1.3229809E7,
// "rvbsn": "한강",
// "gubun": "R"
// }
// ];
export const defaultInitOperFcltState: OperFcltState = {
// operFcltStatus: initialOperFclt,
operFcltStatus: null,
};
export const createOperFcltStore = (initState: OperFcltState = defaultInitOperFcltState) =>
createStore<OperFcltStore>()((set) => ({
...initState,
setOperFcltStatus: (operFcltStatus) => set(() => ({ operFcltStatus })),
}));
export const operFcltStore = createOperFcltStore();
export const initailMapOperFcltData: mapOperFcltType = {
colTitle: {cnt:"개소수",rt:"가동률"}, //헤더 영역
data: [
{name: "금강", cnt: 710, rt: 100,},
{name: "한강", cnt: 983, rt: 100,},
{name: "낙동강", cnt: 1032, rt: 100,},
{name: "영산강", cnt: 1261, rt: 100,},
]
};
type mapOperFcltState = {
mapOperFclt: mapOperFcltItem[];
setMapOperFclt: (mapOperFclt: mapOperFcltItem[]) => void;
};
export const useMapOperFcltStore = create<mapOperFcltState>((set) => ({
mapOperFclt: initailMapOperFcltData.data,
setMapOperFclt: (newMapOperFclt) => set({ mapOperFclt: newMapOperFclt }),
}));
export const initailMapReuseMapData: mapReuseMapType = {
colTitle: {reuseAmnt: "하수재이용량",cnt : "시설수" }, //헤더 영역
data: [
{name: "금강", reuseAmnt: 17099, cnt: 3181},
{name: "한강", reuseAmnt: 17099, cnt: 3181},
{name: "낙동강", reuseAmnt: 14745, cnt: 3100},
{name: "영산강", reuseAmnt: 5073, cnt: 1662},
]
};
type mapReuseMapState = {
mapData: ReuseMapContent[];
setMapData: (mapData: ReuseMapContent[]) => void;
};
export const useMapReuseMapStore = create<mapReuseMapState>((set) => ({
mapData: initailMapReuseMapData.data,
setMapData: (newMapOperFclt) => set({ mapData: newMapOperFclt }),
}));
/**
* OperFcltContent[]를 지도 팝업 데이터 형태인 mapOperFcltItem[]으로 변환합니다.
* @param data API 응답 데이터
* @returns 유역별 개소수 및 가동률이 계산된 데이터
*/
function transformToMapData(data: OperFcltContent[]): mapOperFcltItem[] {
const basinData = new Map<string, { operatingCpc: number; totalCpc: number; count: number }>();
// 1. 유역별로 가동/전체 용량 및 개소수 집계
for (const item of data) {
if (!item.rvbsn) continue;
const basin = basinData.get(item.rvbsn) || { operatingCpc: 0, totalCpc: 0, count: 0 };
basin.totalCpc += item.fctCpc;
if (item.gubun === 'R') {
basin.operatingCpc += item.fctCpc;
basin.count = item.cnt; // 'R' 구분 데이터의 개소수를 사용
}
basinData.set(item.rvbsn, basin);
}
// 2. mapOperFcltItem 형태로 변환
const result: mapOperFcltItem[] = [];
for (const [name, { operatingCpc, totalCpc, count }] of basinData.entries()) {
const rt = totalCpc > 0 ? Math.round((operatingCpc / totalCpc) * 100) : 0;
result.push({ name, cnt: count, rt });
}
return result;
}
//수계별 유입ㆍ방류수질 분석
export type OperWqltState = {
operWqltStatus: OperWqltContent[] | [];
};
export type OperWqltStateActions = {
setOperWqltStatus: (operWqltStatus: OperWqltContent[] | []) => void;
};
export type OperWqltStore = OperWqltState & OperWqltStateActions;
const initialWqltData: OperWqltContent[] = [
{ rvbsn: '금강', yr: '2020', sumInflBod: 97975, sumInflToc: 59900, sumInflSs: 103564, sumInflTp: 2879, sumInflTn: 27062, sumEfflBod: 100143, sumEfflToc: 57805, sumEfflSs: 100942, sumEfflTp: 2901, sumEfflTn: 26796, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
{ rvbsn: '금강', yr: '2021', sumInflBod: 97943, sumInflToc: 55043, sumInflSs: 102727, sumInflTp: 2866, sumInflTn: 26237, sumEfflBod: 100143, sumEfflToc: 57805, sumEfflSs: 100942, sumEfflTp: 2901, sumEfflTn: 26796, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
{ rvbsn: '금강', yr: '2022', sumInflBod: 104513, sumInflToc: 58471, sumInflSs: 96534, sumInflTp: 2960, sumInflTn: 27089, sumEfflBod: 100143, sumEfflToc: 57805, sumEfflSs: 100942, sumEfflTp: 2901, sumEfflTn: 26796, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
{ rvbsn: '낙동강', yr: '2020', sumInflBod: 127321, sumInflToc: 79241, sumInflSs: 120184, sumInflTp: 4004, sumInflTn: 35373, sumEfflBod: 128919, sumEfflToc: 69837, sumEfflSs: 123866, sumEfflTp: 4773, sumEfflTn: 36740, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
{ rvbsn: '낙동강', yr: '2021', sumInflBod: 127792, sumInflToc: 61919, sumInflSs: 123625, sumInflTp: 4157, sumInflTn: 35270, sumEfflBod: 128919, sumEfflToc: 69837, sumEfflSs: 123866, sumEfflTp: 4773, sumEfflTn: 36740, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
{ rvbsn: '낙동강', yr: '2022', sumInflBod: 131644, sumInflToc: 68350, sumInflSs: 127789, sumInflTp: 6159, sumInflTn: 39578, sumEfflBod: 128919, sumEfflToc: 69837, sumEfflSs: 123866, sumEfflTp: 4773, sumEfflTn: 36740, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
{ rvbsn: '영산강', yr: '2020', sumInflBod: 141092, sumInflToc: 97019, sumInflSs: 140610, sumInflTp: 14032, sumInflTn: 45880, sumEfflBod: 132293, sumEfflToc: 83642, sumEfflSs: 140673, sumEfflTp: 8419, sumEfflTn: 46126, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
{ rvbsn: '영산강', yr: '2021', sumInflBod: 131004, sumInflToc: 74582, sumInflSs: 141129, sumInflTp: 5682, sumInflTn: 45056, sumEfflBod: 132293, sumEfflToc: 83642, sumEfflSs: 140673, sumEfflTp: 8419, sumEfflTn: 46126, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
{ rvbsn: '영산강', yr: '2022', sumInflBod: 124784, sumInflToc: 79326, sumInflSs: 140281, sumInflTp: 5543, sumInflTn: 47443, sumEfflBod: 132293, sumEfflToc: 83642, sumEfflSs: 140673, sumEfflTp: 8419, sumEfflTn: 46126, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
{ rvbsn: '한강', yr: '2020', sumInflBod: 146258, sumInflToc: 92931, sumInflSs: 151875, sumInflTp: 4151, sumInflTn: 36537, sumEfflBod: 152099, sumEfflToc: 84360, sumEfflSs: 153111, sumEfflTp: 8443, sumEfflTn: 35571, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
{ rvbsn: '한강', yr: '2021', sumInflBod: 150983, sumInflToc: 77755, sumInflSs: 153838, sumInflTp: 4098, sumInflTn: 34946, sumEfflBod: 152099, sumEfflToc: 84360, sumEfflSs: 153111, sumEfflTp: 8443, sumEfflTn: 35571, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
{ rvbsn: '한강', yr: '2022', sumInflBod: 159057, sumInflToc: 82394, sumInflSs: 153619, sumInflTp: 17080, sumInflTn: 35231, sumEfflBod: 152099, sumEfflToc: 84360, sumEfflSs: 153111, sumEfflTp: 8443, sumEfflTn: 35571, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
{ rvbsn: '', yr: '2020', sumInflBod: 55280, sumInflToc: 35027, sumInflSs: 55125, sumInflTp: 1695, sumInflTn: 15442, sumEfflBod: 65532, sumEfflToc: 36455, sumEfflSs: 65256, sumEfflTp: 2830, sumEfflTn: 18291, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
{ rvbsn: '', yr: '2021', sumInflBod: 66809, sumInflToc: 33841, sumInflSs: 65743, sumInflTp: 2015, sumInflTn: 17674, sumEfflBod: 65532, sumEfflToc: 36455, sumEfflSs: 65256, sumEfflTp: 2830, sumEfflTn: 18291, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
{ rvbsn: '', yr: '2022', sumInflBod: 74505, sumInflToc: 40498, sumInflSs: 74899, sumInflTp: 4780, sumInflTn: 21756, sumEfflBod: 65532, sumEfflToc: 36455, sumEfflSs: 65256, sumEfflTp: 2830, sumEfflTn: 18291, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
];
export const defaultInitOperWqltState: OperWqltState = {
operWqltStatus: initialWqltData,
};
export const createOperWqltStore = (initState: OperWqltState = defaultInitOperWqltState) =>
createStore<OperWqltStore>()((set) => ({
...initState,
setOperWqltStatus: (operWqltStatus) => set(() => ({ operWqltStatus })),
}));
export const operWqltStore = createOperWqltStore();
// 물재이용 현황
export type ReuseState = {
reuseStatus: ReuseStatusContent[] | null;
};
export type ReuseStateActions = {
setReuseStatus: (reuseStatus: ReuseStatusContent[] | null) => void;
};
export type ReuseStore = ReuseState & ReuseStateActions;
const initialReuseStatusData: ReuseStatusContent[] = [
{ yr: '2021', sewage: 7335803, reclaimed: 254805510.3, rainwater: 14049224.5 },
{ yr: '2022', sewage: 7378907, reclaimed: 243457788.8, rainwater: 15128093.4 },
{ yr: '2023', sewage: 7537151, reclaimed: 222475997, rainwater: 13944732.5 },
{ yr: '2024', sewage: 7479136, reclaimed: 265625917.7, rainwater: 13305309.5 },
];
export const defaultInitReuseState: ReuseState = {
reuseStatus: initialReuseStatusData,
};
export const createReuseStore = (initState: ReuseState = defaultInitReuseState) =>
createStore<ReuseStore>()((set) => ({
...initState,
setReuseStatus: (reuseStatus) => set(() => ({ reuseStatus })),
}));
export const reuseStore = createReuseStore();
/**
* 두 객체 또는 배열의 깊은 비교를 수행합니다.
* @param obj1 비교할 첫 번째 객체
* @param obj2 비교할 두 번째 객체
* @returns 두 객체의 내용이 같으면 true, 그렇지 않으면 false
*/
// function deepEqual(obj1: any, obj2: any): boolean {
// if (obj1 === obj2) {
// return true;
// }
// if (obj1 == null || typeof obj1 !== 'object' || obj2 == null || typeof obj2 !== 'object') {
// return false;
// }
// const keys1 = Object.keys(obj1);
// const keys2 = Object.keys(obj2);
// if (keys1.length !== keys2.length) {
// return false;
// }
// for (const key of keys1) {
// if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {
// return false;
// }
// }
// // 배열인 경우 순서와 내용이 모두 동일한지 확인
// if (Array.isArray(obj1) && Array.isArray(obj2)) {
// if (obj1.length !== obj2.length) return false;
// for (let i = 0; i < obj1.length; i++) {
// if (!deepEqual(obj1[i], obj2[i])) return false;
// }
// }
// return true;
// }
export class OperationApiService implements OperationService {
// 기준년도 조회
public async getStandardYear(): Promise<string> {
const { year, setYear } = yearStore.getState();
if (year) {
return year;
}
const response = await client.get<{
status: number;
data: string;
count: number;
}>(ENDPOINT.OPERATION_SERVICE.MAIN_YEAR);
const newYear = response.data.data || '2024';
setYear(newYear);
return newYear;
}
// 수계별 시설운영 개소수 및 가동률
public async getDashOperCntRt(year: string): Promise<OperFcltContent[]> {
const { operFcltStatus, setOperFcltStatus } = operFcltStore.getState();
const { setMapOperFclt } = useMapOperFcltStore.getState();
// 간단한 캐싱: 데이터가 이미 있으면 API 호출 생략
if (operFcltStatus && operFcltStatus.length > 0) {
return operFcltStatus;
}
const response = await client.get<{
status: number;
data: OperFcltContent[];
count: number;
}>(ENDPOINT.OPERATION_SERVICE.OPER_CNTRT, {
params: {
year,
},
});
const list = (response.data.data ?? []).map((item) => ({
...item,
cnt: Number(item.cnt),
fctCpc: Number(item.fctCpc),
}));
setOperFcltStatus(list);
setMapOperFclt(transformToMapData(list));
return list;
}
// 수계별 유입ㆍ방류수질 분석
public async getDashOperWqlt(year: string): Promise<OperWqltContent[]> {
const response = await client.get<{
status: number;
data: OperWqltContent[];
count: number;
}>(ENDPOINT.OPERATION_SERVICE.OPER_WQLT, {
params: {
year,
},
});
const list = (response.data.data ?? []).map((item) => ({
...item,
}));
return list;
}
// 물재이용 현황
public async getReuseStatus(): Promise<ReuseStatusContent[]> {
// API 호출 시에는 항상 최신 데이터를 가져오도록 캐싱 로직을 제거하거나 수정할 수 있습니다.
// 여기서는 기존 캐싱 로직을 유지하되, 초기 데이터가 아닌 경우에만 API를 호출하도록 합니다.
const { reuseStatus, setReuseStatus } = reuseStore.getState();
if (reuseStatus && reuseStatus.length > 0 && reuseStatus !== defaultInitReuseState.reuseStatus) {
return reuseStatus;
}
const response = await client.get<{
status: number;
data: ReuseStatusContent[];
count: number;
}>(ENDPOINT.OPERATION_SERVICE.REUSE_STATUS);
const list = (response.data.data ?? []).map((item) => ({
...item,
reclaimed: Number(item.reclaimed),
rainwater: Number(item.rainwater),
}));
setReuseStatus(list);
return list;
}
// 수계별 하수재이용량, 시설수
public async getReuseMap(year: string): Promise<ReuseMapContent[]> {
const response = await client.get<{
status: number;
data: ReuseMapContent[];
count: number;
}>(ENDPOINT.OPERATION_SERVICE.REUSE_MAP, {
params: {
year,
},
});
const list = (response.data.data ?? []).map((item) => ({
...item,
}));
return list;
}
}
export const OperationApi = new OperationApiService();
@@ -0,0 +1,66 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageableList } from "@/shared/model";
export interface ScheduleParams {
siteId?: string;
schdlTtl?: string;
schdlSeCd?: string;
schdlBgngYmd?: string;
schdlEndYmd?: string;
reptYn?: string;
commSchdlYn?: string;
}
export interface Schedule {
schdlSn: number;
siteId: string;
schdlTtl: string;
schdlSeCd: string;
schdlSeNm?: string;
schdlBgngDt: string;
schdlEndDt: string;
reptYn: string;
commSchdlYn: string;
schdlCn?: string;
actualDate?: string;
atflCd?: string | null;
}
export const getScheduleList = async (params?: ScheduleParams): Promise<PageableList<Schedule>> => {
const response = await client.get<Schedule[]>(
ENDPOINT.CMS_SERVICE.SCHEDULES,
{ params },
);
const data = response.data;
return {
content: data as Schedule[],
pageable: {
sort: {
sorted: false,
unsorted: true,
empty: true,
},
offset: 0,
pageNumber: 0,
pageSize: data.length as number,
paged: false,
unpaged: true,
},
sort: {
sorted: false,
unsorted: true,
empty: true,
},
totalPages: 1,
totalElements: data.length,
number: 0,
numberOfElements: data.length,
size: data.length,
first: true,
last: true,
empty: data.length === 0,
};
};
@@ -0,0 +1,189 @@
// apps/cm/src/pages/dashboard/api/sn-service.ts
import { client } from "@/shared/api";
import { create } from "zustand";
import { ENDPOINT } from "@/shared/config";
import type { PplLengthContent, SewerageService } from "../model/sn-interface";
import type { OldPplChartData, mapPplOldType, mapPplOldItem } from "../model/sn-interface";
export type SnState = {
snStatus: PplLengthContent[] | null;
};
export type SnStateActions = {
setSnStatus: (snStatus: PplLengthContent[] | null) => void;
};
export type SnStore = SnState & SnStateActions;
const initialUsePplChartData: OldPplChartData = {
labels: ["금강", "한강", "낙동강", "영산강"],
datasets: [
{
label: "기타",
data: [1966, 15480, 6957, 1937],
backgroundColor: "rgba(255, 178, 150, 0.9)"
},
{
label: "오수관로",
data: [8074, 15879, 16337, 5671],
backgroundColor: "rgba(180, 240, 190, 0.95)"
},
{
label: "우수관로",
data: [4838, 12893, 10492, 4281],
backgroundColor: "rgba(170, 210, 245, 0.95)"
}
]
};
const initialOldPplChartData: OldPplChartData = {
labels: ["금강", "한강", "낙동강", "영산강"],
datasets: [
{
label: "20-25년",
data: [1140, 3181, 3100, 1662],
backgroundColor: "#3B82F6",
borderRadius: 4
},
{
label: "25-30년",
data: [1391, 3615, 1814, 1411],
backgroundColor: "#EF4444",
borderRadius: 4
},
{
label: "30년이상",
data: [2014, 10303, 9831, 2000],
backgroundColor: "#F87171",
borderRadius: 4
}
]
};
type OldPplStoreState = {
chartData: OldPplChartData;
setChartData: (chartData: OldPplChartData) => void;
};
export const useOldPplStore = create<OldPplStoreState>((set) => ({
chartData: initialOldPplChartData,
setChartData: (chartData) => set(() => ({ chartData })),
}));
export const useUsePplStore = create<OldPplStoreState>((set) => ({
chartData: initialUsePplChartData,
setChartData: (chartData) => set(() => ({ chartData })),
}));
export const initailMapOldPplData: mapPplOldType = {
colTitle: { totalLen: "총길이", pplLen2025: "20~25년", pplLen2530: "25~30년", pplLen30: "30년 이상" }, //헤더 영역
data: [
{ name: "금강", totalLen: 4545, pplLen2025: 1140, pplLen2530: 1391, pplLen30: 2014 },
{ name: "한강", totalLen: 17099, pplLen2025: 3181, pplLen2530: 3615, pplLen30: 10303 },
{ name: "낙동강", totalLen: 14745, pplLen2025: 3100, pplLen2530: 1814, pplLen30: 9831 },
{ name: "영산강", totalLen: 5073, pplLen2025: 1662, pplLen2530: 1411, pplLen30: 2000 },
]
};
type mapPplOldState = {
mapData: mapPplOldItem[];
setMapData: (mapData: mapPplOldItem[]) => void;
};
export const useMapOldPplStore = create<mapPplOldState>((set) => ({
mapData: initailMapOldPplData.data,
setMapData: (newMapData) => set({ mapData: newMapData }),
}));
/**
* OldPplChartData를 지도 팝업 데이터 형태인 mapPplOldItem[]으로 변환합니다.
* @param chartData 차트 데이터
* @returns 유역별 노후관로 현황 데이터
*/
export function transformChartDataToMapData(chartData: OldPplChartData): mapPplOldItem[] {
const mapData: mapPplOldItem[] = [];
const { labels, datasets } = chartData;
const pplLen2025Data = datasets.find(d => d.label === '20-25년')?.data || [];
const pplLen2530Data = datasets.find(d => d.label === '25-30년')?.data || [];
const pplLen30Data = datasets.find(d => d.label === '30년이상')?.data || [];
labels.forEach((label, index) => {
const pplLen2025 = pplLen2025Data[index] || 0;
const pplLen2530 = pplLen2530Data[index] || 0;
const pplLen30 = pplLen30Data[index] || 0;
const totalLen = pplLen2025 + pplLen2530 + pplLen30;
mapData.push({
name: label,
totalLen,
pplLen2025,
pplLen2530,
pplLen30,
});
});
return mapData;
}
export class SewerageApiService implements SewerageService {
// 기간별 관로 총길이
public async getPplLength(instlYmdFrom: string, instlYmdTo: string): Promise<PplLengthContent[]> {
const response = await client.get<{
PplLengthList: PplLengthContent[];
}>(ENDPOINT.SEWERAGE_SERVICE.PPL_LENGTH, {
params: {
instlYmdFrom,
instlYmdTo,
},
});
const list = (response.data.PplLengthList ?? []).map((item) => ({
...item,
sumPrlgVl: Number(item.sumPrlgVl),
}));
return list;
}
// 기간별 용도별 관로 총길이 : useCd - SBA004 오수관, SBA003 우수관
public async getPplUseLength(instlYmdFrom: string, instlYmdTo: string, useCd: string): Promise<PplLengthContent[]> {
const response = await client.get<{
PplLengthList: PplLengthContent[];
}>(ENDPOINT.SEWERAGE_SERVICE.PPL_USE_LENGTH, {
params: {
instlYmdFrom,
instlYmdTo,
useCd,
},
});
const list = (response.data.PplLengthList ?? []).map((item) => ({
...item,
sumPrlgVl: Number(item.sumPrlgVl),
}));
return list;
}
// 유역별 용도별 관로 길이(km) MV조회 : usgCd - SBA004 오수관, SBA003 우수관
public async getMvUsgPplLength(): Promise<PplLengthContent[]> {
const response = await client.get<{
PplLengthList: PplLengthContent[];
}>(ENDPOINT.SEWERAGE_SERVICE.MV_USG_PPL_LENGTH);
const list = (response.data.PplLengthList ?? []).map((item) => ({
...item,
sumPrlgVl: Number(item.sumPrlgVl),
}));
return list;
}
// 유역별 노후도별 관로 길이(km) MV조회 : yearGroup - 20년~25년미만,25년~30년미만,30년이상
public async getMvOldPplLength(): Promise<PplLengthContent[]> {
const response = await client.get<{
PplLengthList: PplLengthContent[];
}>(ENDPOINT.SEWERAGE_SERVICE.MV_OLD_PPL_LENGTH);
const list = (response.data.PplLengthList ?? []).map((item) => ({
...item,
sumPrlgVl: Number(item.sumPrlgVl),
}));
return list;
}
}
export const SewerageApi = new SewerageApiService();
@@ -0,0 +1,53 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageableList } from "@/shared/model";
export interface Support {
total: number;
ongoing: number;
han: number;
youngsang: number;
geum: number;
nakdong: number;
completed: number;
type: string;
}
export const getSupportList = async (): Promise<PageableList<Support>> => {
const response = await client.get<Support[]>(
`${ENDPOINT.IC_SERVICE.GROUPING_STATISTICS}`,
);
const data = response.data;
console.log("getSupportList data:", data);
return {
content: data as Support[],
pageable: {
sort: {
sorted: false,
unsorted: true,
empty: true,
},
offset: 0,
pageNumber: 0,
pageSize: data.length as number,
paged: false,
unpaged: true,
},
sort: {
sorted: false,
unsorted: true,
empty: true,
},
totalPages: 1,
totalElements: data.length,
number: 0,
numberOfElements: data.length,
size: data.length,
first: true,
last: true,
empty: data.length === 0,
};
};
@@ -1,425 +0,0 @@
import React, { useEffect, useRef } from "react";
import styles from "./KoreaBasinMap.module.css"; // 추가
const SVG_URL = "/images/region_map.svg"; // public 폴더에 region_map.svg 위치 필요
const MANUAL_LABEL_POS = {
han: { x: 0.52, y: 0.14 },
geum: { x: 0.28, y: 0.47 },
nakdong: { x: 0.64, y: 0.47 },
yeongsan: { x: 0.18, y: 0.77 },
};
const BASIN_DATA = {
han: {
name: "한강",
colTitle: "시설수",
rows: [
["500m³ 미만", 100000000],
["500m³ 이상", 100000000],
],
},
geum: {
name: "금강",
colTitle: "시설수",
rows: [
["500m³ 미만", 100000000],
["500m³ 이상", 100000000],
],
},
nakdong: {
name: "낙동강",
colTitle: "시설수",
rows: [
["500m³ 미만", 100000000],
["500m³ 이상", 100000000],
],
},
yeongsan: {
name: "영산강",
colTitle: "시설수",
rows: [
["500m³ 미만", 100000000],
["500m³ 이상", 100000000],
],
},
};
const BASIN_IDS = [
{ key: "han", labels: ["한강", "han"] },
{ key: "geum", labels: ["금강", "geum"] },
{ key: "nakdong", labels: ["낙동강", "nakdong"] },
{ key: "yeongsan", labels: ["영산강", "yeongsan"] },
];
function fmt(n: number) {
return new Intl.NumberFormat("ko-KR").format(n);
}
export function KoreaBasinMap() {
const stageRef = useRef<HTMLDivElement>(null);
const svgLayerRef = useRef<HTMLDivElement>(null);
const labelsLayerRef = useRef<HTMLDivElement>(null);
const popupRef = useRef<HTMLDivElement>(null);
const badgeRef = useRef<HTMLDivElement>(null);
const rowsRef = useRef<HTMLTableSectionElement>(null);
const colTitleRef = useRef<HTMLTableCellElement>(null);
useEffect(() => {
let svg: SVGSVGElement | null = null;
let gs: SVGGElement[] = [];
let labels: HTMLDivElement[] = [];
console.log("Loading SVG from:", SVG_URL);
fetch(SVG_URL)
.then((r) => {
console.log("SVG fetch response:", r.status, r.statusText);
if (!r.ok) throw new Error(`HTTP error! status: ${r.status}`);
return r.text();
})
.then((txt) => {
console.log("SVG content loaded, length:", txt.length);
if (!svgLayerRef.current) {
console.error("svgLayerRef.current is null");
return;
}
svgLayerRef.current.innerHTML = txt;
svg = svgLayerRef.current.querySelector("svg");
if (!svg) {
console.error("SVG element not found in loaded content");
return;
}
console.log("SVG element found:", svg);
// viewBox/크기 세팅
if (!svg.hasAttribute("viewBox")) {
const bb = svg.getBBox();
svg.setAttribute(
"viewBox",
[bb.x, bb.y, bb.width, bb.height].join(" "),
);
}
const vb = (svg.getAttribute("viewBox") || "").split(/\s+/).map(Number);
if (vb.length === 4 && vb[3]! > 0 && stageRef.current) {
stageRef.current!.style.aspectRatio = String(vb[2]! / vb[3]!);
}
svg!.setAttribute("preserveAspectRatio", "xMidYMid meet");
svg.style.width = "100%";
svg.style.height = "100%";
// 1) 지도 그룹 수집
gs = [];
console.log("Looking for basin groups in SVG...");
BASIN_IDS.forEach((item) => {
let g: SVGGElement | null = null;
for (const id of item.labels) {
g = svg!.getElementById(id) as SVGGElement | null;
if (g) {
console.log(`Found group for ${item.key} with id '${id}':`, g);
break;
}
}
if (!g) {
console.warn(`No group found for ${item.key} with labels:`, item.labels);
return;
}
g.classList.add("basin");
(g as SVGGElement & { dataset: { key: string; label: string } }).dataset.key = item.key;
(g as SVGGElement & { dataset: { key: string; label: string } }).dataset.label = item.labels[0] || '';
// 패스에 기본 스타일 적용
const paths = g.querySelectorAll('path');
paths.forEach((path) => {
path.style.fill = 'rgba(0, 150, 255, 0.2)';
path.style.stroke = '#0066cc';
path.style.strokeWidth = '1';
path.style.vectorEffect = 'non-scaling-stroke';
});
gs.push(g);
});
console.log(`Found ${gs.length} basin groups`);
// 각 유역 그룹의 패스 정보 확인
gs.forEach((g, index) => {
const paths = g.querySelectorAll('path');
console.log(`Group ${index} (${(g as SVGGElement & { dataset: { key: string } }).dataset.key}):`, {
element: g,
pathCount: paths.length,
paths: Array.from(paths).map(p => ({
className: p.className.baseVal,
d: p.getAttribute('d')?.substring(0, 50) + '...'
}))
});
});
// 2) 함수들 먼저 정의
const clear = () => {
gs.forEach((el) => el.classList.remove("active"));
if (popupRef.current) {
if (styles.show) {
popupRef.current.classList.remove(styles.show);
}
popupRef.current.style.opacity = "0";
popupRef.current.style.visibility = "hidden";
}
if (badgeRef.current) {
if (styles.show) {
badgeRef.current.classList.remove(styles.show);
}
badgeRef.current.style.opacity = "0";
badgeRef.current.style.visibility = "hidden";
}
labels.forEach((l) => l.classList.remove("active"));
};
function showFor(g: SVGGElement) {
console.log("showFor called for:", g);
const key = (g as SVGGElement & { dataset: { key: string; label: string } }).dataset.key;
const data = (BASIN_DATA as Record<string, { name: string; colTitle: string; rows: (string | number)[][] }>)[key] || {
name: (g as SVGGElement & { dataset: { label: string } }).dataset.label,
colTitle: "",
rows: [],
};
console.log("Data for popup:", data);
if (badgeRef.current) badgeRef.current.textContent = data.name;
if (colTitleRef.current)
colTitleRef.current.textContent = data.colTitle || "???";
if (rowsRef.current)
rowsRef.current.innerHTML = data.rows
.map(
(r: (string | number)[]) =>
`<tr><td>${r[0]}</td><td style="text-align:right">${fmt(Number(r[1]) || 0)}</td></tr>`,
)
.join("");
if (!stageRef.current) return;
const sRect = stageRef.current.getBoundingClientRect(),
r = g.getBoundingClientRect();
const cx = (r.left + r.right) / 2 - sRect.left;
const ty = r.top - sRect.top;
console.log("Popup position:", { cx, ty });
if (badgeRef.current) {
badgeRef.current.style.left = cx + "px";
badgeRef.current.style.top = ty + 24 + "px";
}
if (popupRef.current) {
popupRef.current.style.left = cx + "px";
popupRef.current.style.top = ty + 6 + "px";
popupRef.current.style.opacity = "1";
popupRef.current.style.visibility = "visible";
if (styles.show) {
popupRef.current.classList.add(styles.show);
}
console.log("Added show class to popup:", popupRef.current.classList.toString());
}
if (badgeRef.current && styles.show) {
badgeRef.current.classList.add(styles.show);
}
if (popupRef.current) {
const pRect = popupRef.current.getBoundingClientRect();
const x = Math.max(
12 + pRect.width / 2,
Math.min(cx, sRect.width - 12 - pRect.width / 2),
);
popupRef.current.style.left = x + "px";
if (badgeRef.current) badgeRef.current.style.left = x + "px";
}
}
function showForWithLabel(g: SVGGElement) {
console.log("showForWithLabel called for:", g);
showFor(g);
const idx = gs.indexOf(g);
labels.forEach((l) => l.classList.remove("active"));
if (idx >= 0) labels[idx]!.classList.add("active");
}
// 3) 라벨 DOM 생성
labels = [];
if (labelsLayerRef.current) {
labelsLayerRef.current.innerHTML = "";
gs.forEach((g) => {
const el = document.createElement("div");
el.className = styles.basinLabel || '';
el.textContent = (g as SVGGElement & { dataset: { label: string } }).dataset.label || '';
labelsLayerRef.current!.appendChild(el);
labels.push(el);
});
}
// 3) 라벨 위치
function positionLabels() {
if (!stageRef.current || !svg) return;
const sRect = stageRef.current.getBoundingClientRect();
const inset =
parseFloat(
getComputedStyle(stageRef.current).getPropertyValue(
"--map-inset",
) || "0",
) / 100;
const left = sRect.width * inset,
top = sRect.height * inset;
const w = sRect.width * (1 - inset * 2),
h = sRect.height * (1 - inset * 2);
const [vx, vy, vw, vh] = (
svg!.getAttribute("viewBox") || "0 0 100 100"
)
.split(/\s+/)
.map(Number);
const svgRect = svg.getBoundingClientRect();
const stageRect = stageRef.current.getBoundingClientRect();
const offsetX = svgRect.left - stageRect.left - left;
const offsetY = svgRect.top - stageRect.top - top;
gs.forEach((g, i) => {
const key = (g as SVGGElement & { dataset: { key: string } }).dataset.key;
const manual = (MANUAL_LABEL_POS as Record<string, { x?: number; y?: number; px?: number; py?: number }>)[key];
let px, py;
if (
manual &&
(("x" in manual && "y" in manual) ||
("px" in manual && "py" in manual))
) {
if ("px" in manual) {
px = left + (manual.px || 0) + offsetX;
py = top + (manual.py || 0) + offsetY;
} else {
px = left + w * (manual.x || 0) + offsetX;
py = top + h * (manual.y || 0) + offsetY;
}
} else {
const b = g.getBBox();
const cx = b.x + b.width / 2,
cy = b.y + b.height / 2;
px = ((cx - vx!) / vw!) * w + left + offsetX;
py = ((cy - vy!) / vh!) * h + top + offsetY;
}
labels[i]!.style.left = `${px}px`;
labels[i]!.style.top = `${py + 18}px`;
});
}
// 4) 상호작용 (함수들은 이미 위에서 정의됨)
gs.forEach((g) => {
g.addEventListener("mouseenter", () => {
clear();
g.classList.add("active");
showForWithLabel(g);
});
g.addEventListener("mousemove", () => {
if (g.classList.contains("active")) showForWithLabel(g);
});
g.addEventListener("mouseleave", clear);
g.addEventListener("click", (e) => {
e.stopPropagation();
clear();
g.classList.add("active");
showForWithLabel(g);
});
});
if (stageRef.current) {
stageRef.current.addEventListener("mouseleave", clear);
stageRef.current.addEventListener("click", () => clear());
}
// 5) 실행
positionLabels();
window.addEventListener("resize", positionLabels);
new window.ResizeObserver(positionLabels).observe(stageRef.current!);
})
.catch((error) => {
console.error("Error loading SVG:", error);
if (svgLayerRef.current)
svgLayerRef.current.innerHTML =
`<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #666; font-size: 14px;">지도를 불러올 수 없습니다. (${error.message})</div>`;
});
// cleanup
return () => {
window.removeEventListener("resize", () => {});
};
}, []);
// 스타일은 필요에 따라 tailwind, css 등으로 커스터마이즈하세요.
return (
<div
ref={stageRef}
id="stage"
style={
{
width: "100%",
height: "560px",
position: "relative",
zIndex: 20,
"--map-inset": "0",
} as React.CSSProperties
}
>
<div
className={styles.bgLayer}
id="bgLayer"
/>
<div
ref={svgLayerRef}
id="svgLayer"
className={`${styles.svgLayer} ${styles.svgContainer}`}
style={{ width: "100%", height: "100%" }}
/>
<div
ref={labelsLayerRef}
id="labelsLayer"
className={styles.labelsLayer}
style={{
position: "absolute",
left: 0,
top: 0,
width: "100%",
height: "100%",
pointerEvents: "none",
}}
/>
<div
ref={badgeRef}
id="badge"
className={styles.badge}
style={{ position: "absolute", zIndex: 10, display: "block" }}
/>
<div
ref={popupRef}
id="popup"
className={styles.popup}
style={{
position: "absolute",
zIndex: 20,
background: "#fff",
border: "1px solid #ccc",
borderRadius: 8,
padding: 12,
minWidth: 160,
boxShadow: "0 2px 8px #0002",
display: "block",
opacity: 0,
visibility: "hidden",
}}
>
<table style={{ width: "100%" }}>
<thead>
<tr>
<th>구분</th>
<th ref={colTitleRef} id="colTitle">
시설수
</th>
</tr>
</thead>
<tbody ref={rowsRef} id="rows"></tbody>
</table>
</div>
</div>
);
}
@@ -0,0 +1,502 @@
"use client";
/**
* 유역 지도
*/
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useStore } from "zustand";
import styles from "./korea-basin-map.module.css"; // 추가
import { basinStore } from "../api/fo-service";
import { basinKeyMap,MANUAL_LABEL_POS } from "../model/map-data";
import { BASIN_DATA, BASIN_IDS } from "../model/map-data";
import { svcInfo } from "../model/map-data";
import type { mapOperFcltType, mapReuseMapType} from "../model/fo-interface";
import type { mapPplOldType } from "../model/sn-interface";
import { useMapOperFcltStore, OperationApi, useMapReuseMapStore } from "../api/fo-service";
import { useMapOldPplStore } from "../api/sn-service";
import {Button} from "@common/components";
const SVG_URL = "/images/region_map.svg"; // public 폴더에 region_map.svg 위치 필요
function fmt(n: number) {
return new Intl.NumberFormat("ko-KR").format(n);
}
const mapOperFcltData: mapOperFcltType = {
colTitle: {cnt:"개소수",rt:"가동률"}, //헤더 영역
data: [
{name: "금강", cnt: 710, rt: 100,},
{name: "한강", cnt: 983, rt: 100,},
{name: "낙동강", cnt: 1032, rt: 100,},
{name: "영산강", cnt: 1261, rt: 100,},
]
};
const mapPplOldData: mapPplOldType = {
colTitle: {totalLen: "총길이",pplLen2025: "20~25년",pplLen2530: "25~30년",pplLen30 : "30년 이상" }, //헤더 영역
data: [
{name: "금강", totalLen: 4545, pplLen2025: 1140, pplLen2530: 1391, pplLen30 : 2014},
{name: "한강", totalLen: 17099, pplLen2025: 3181, pplLen2530: 3615, pplLen30 : 10303},
{name: "낙동강", totalLen: 14745, pplLen2025: 3100, pplLen2530: 1814, pplLen30 : 9831},
{name: "영산강", totalLen: 5073, pplLen2025: 1662, pplLen2530: 1411, pplLen30 : 2000},
]
};
const mapReuseMapData: mapReuseMapType = {
colTitle: {reuseAmnt: "하수재이용량",cnt : "시설수" }, //헤더 영역
data: [
{name: "금강", reuseAmnt: 17099, cnt: 3181},
{name: "한강", reuseAmnt: 17099, cnt: 3181},
{name: "낙동강", reuseAmnt: 14745, cnt: 3100},
{name: "영산강", reuseAmnt: 5073, cnt: 1662},
]
};
export function KoreaBasinMap() {
const stageRef = useRef<HTMLDivElement>(null);
const svgLayerRef = useRef<HTMLDivElement>(null);
const labelsLayerRef = useRef<HTMLDivElement>(null);
const popupRef = useRef<HTMLDivElement>(null);
const [services, setServices] = useState(svcInfo);
const [popupContent, setPopupContent] = useState<{ title: string; items: { header: string; data: string }[] }>({
title: '',
items: [],
});
const { selectedBasin, setSelectedBasin } = useStore(basinStore);
const selectedBasinKey = basinKeyMap[selectedBasin];
const {mapOperFclt} = useMapOperFcltStore();
const {mapData: mapPplOld} = useMapOldPplStore();
const {setMapData: setMapReuseMapData} = useMapReuseMapStore();
mapOperFcltData.data = mapOperFclt;
mapPplOldData.data = mapPplOld;
// mapReuseMapData.data = mapReuseMap;
let standardYear = '2024';
useEffect(() => {
const fetchData = async () => {
try {
standardYear = await OperationApi.getStandardYear();
const mapReuseMapRslt = await OperationApi.getReuseMap(standardYear);
setMapReuseMapData(mapReuseMapRslt);
mapReuseMapData.data = mapReuseMapRslt;
} catch (e) {
console.error("하수재이용량 map 데이터 조회 실패", e);
}
};
fetchData();
}, []);
// 유역정보
const showFor = useCallback((g: SVGGElement) => {
const activeService = services.find(s => s.active);
if (!activeService) return;
const key = (g as SVGGElement & { dataset: { key: string; label: string } }).dataset.key;
const basinName = BASIN_DATA[key as keyof typeof BASIN_DATA]?.name || (g as SVGGElement & { dataset: { label: string } }).dataset.label;
const newPopupContent: { title: string; items: { header: string; data: string }[] } = { title: '', items: [] };
if (activeService.svcId === 'fo') { // 운영실태
const item = mapOperFcltData.data.find(d => d.name === basinName);
newPopupContent.title = `${standardYear}년도 ${basinName}유역 시설운영 현황`;
newPopupContent.items.push({
header: mapOperFcltData.colTitle.cnt,
data: item ? `${fmt(item.cnt)}개소` : '-',
});
newPopupContent.items.push({
header: mapOperFcltData.colTitle.rt,
data: item ? `${item.rt}%` : '-',
});
} else if (activeService.svcId === 'sn') { // 하수관로
const item = mapPplOldData.data.find(d => d.name === basinName);
newPopupContent.title = `${standardYear}년도 ${basinName}유역 노후하수관로 현황`;
const colTitles = mapPplOldData.colTitle;
newPopupContent.items.push({
header: colTitles.totalLen,
data: item ? `${fmt(item.totalLen)}km` : '-',
});
newPopupContent.items.push({
header: colTitles.pplLen2025,
data: item ? `${fmt(item.pplLen2025)}km` : '-',
});
newPopupContent.items.push({
header: colTitles.pplLen2530,
data: item ? `${fmt(item.pplLen2530)}km` : '-',
});
newPopupContent.items.push({
header: colTitles.pplLen30,
data: item ? `${fmt(item.pplLen30)}km` : '-',
});
} else if (activeService.svcId === 'wr') { // 물재이용
const item = mapReuseMapData.data.find(d => d.name === basinName);
newPopupContent.title = `${standardYear}년도 ${basinName}유역 물재이용 현황`;
newPopupContent.items.push({
header: mapReuseMapData.colTitle.reuseAmnt,
data: item ? `${fmt(item.reuseAmnt)}m³` : '-',
});
newPopupContent.items.push({
header: mapReuseMapData.colTitle.cnt,
data: item ? `${fmt(item.cnt)}개소` : '-',
});
}
setPopupContent(newPopupContent);
if (!stageRef.current) return;
const sRect = stageRef.current.getBoundingClientRect(),
r = g.getBoundingClientRect();
const cx = (r.left + r.right) / 2 - sRect.left;
const ty = r.top - sRect.top;
if (popupRef.current) {
popupRef.current.style.left = cx + "px";
popupRef.current.style.top = ty + 6 + "px";
popupRef.current.style.opacity = "1";
popupRef.current.style.visibility = "visible";
if (styles.show) {
popupRef.current.classList.add(styles.show);
}
}
if (popupRef.current) {
const pRect = popupRef.current.getBoundingClientRect();
const x = Math.max(
12 + pRect.width / 2,
Math.min(cx, sRect.width - 12 - pRect.width / 2),
);
popupRef.current.style.left = x + "px";
}
}, [services, mapOperFclt]);
// selectedBasinKey prop이 변경될 때 지도를 업데이트하는 useEffect
useEffect(() => {
if (selectedBasinKey) {
const svg = svgLayerRef.current?.querySelector("svg");
if (!svg) return;
// 모든 유역의 활성화 상태를 초기화합니다.
svg.querySelectorAll('.basin.active').forEach(el => el.classList.remove('active'));
const targetGroup = svg.querySelector(`.basin[data-key="${selectedBasinKey}"]`) as SVGGElement | null;
targetGroup?.classList.add('active');
}
}, [selectedBasinKey]);
useEffect(() => {
let svg: SVGSVGElement | null = null;
let gs: SVGGElement[] = [];
let labels: HTMLDivElement[] = [];
const clear = () => {
gs.forEach((el) => el.classList.remove("active"));
if (popupRef.current) {
if (styles.show) {
popupRef.current.classList.remove(styles.show);
}
popupRef.current.style.opacity = "0";
}
labels.forEach((l) => l.classList.remove("active"));
};
// const positionLabels = () => {
// // This function's content is moved inside the fetch callback
// };
// console.log("Loading SVG from:", SVG_URL);
fetch(SVG_URL)
.then((r) => {
// console.log("SVG fetch response:", r.status, r.statusText);
if (!r.ok) throw new Error(`HTTP error! status: ${r.status}`);
return r.text();
})
.then((txt) => {
// console.log("SVG content loaded, length:", txt.length);
if (!svgLayerRef.current) {
console.error("svgLayerRef.current is null");
return;
}
svgLayerRef.current.innerHTML = txt;
svg = svgLayerRef.current.querySelector("svg");
if (!svg) {
console.error("SVG element not found in loaded content");
return;
}
// console.log("SVG element found:", svg);
// viewBox/크기 세팅
if (!svg.hasAttribute("viewBox")) {
const bb = svg.getBBox();
svg.setAttribute(
"viewBox",
[bb.x, bb.y, bb.width, bb.height].join(" "),
);
}
const vb = (svg.getAttribute("viewBox") || "").split(/\s+/).map(Number);
if (vb.length === 4 && vb[3]! > 0 && stageRef.current) {
stageRef.current!.style.aspectRatio = String(vb[2]! / vb[3]!);
}
svg!.setAttribute("preserveAspectRatio", "xMidYMid meet");
svg.style.width = "100%";
svg.style.height = "100%";
// 1) 지도 그룹 수집
gs = [];
// console.log("Looking for basin groups in SVG...");
BASIN_IDS.forEach((item) => {
let g: SVGGElement | null = null;
for (const id of item.labels) {
g = svg!.getElementById(id) as SVGGElement | null;
if (g) {
// console.log(`Found group for ${item.key} with id '${id}':`, g);
break;
}
}
if (!g) {
console.warn(`No group found for ${item.key} with labels:`, item.labels);
return;
}
g.classList.add("basin");
(g as SVGGElement & { dataset: { key: string; label: string } }).dataset.key = item.key;
(g as SVGGElement & { dataset: { key: string; label: string } }).dataset.label = item.labels[0] || '';
// 패스에 기본 스타일 적용
const paths = g.querySelectorAll('path');
paths.forEach((path) => {
path.style.fill = 'rgba(0, 150, 255, 0.2)';
path.style.stroke = '#0066cc';
path.style.strokeWidth = '1';
path.style.vectorEffect = 'non-scaling-stroke';
});
gs.push(g);
});
// console.log(`Found ${gs.length} basin groups`);
// 각 유역 그룹의 패스 정보 확인
// gs.forEach((g, index) => {
// const paths = g.querySelectorAll('path');
// // console.log(`Group ${index} (${(g as SVGGElement & { dataset: { key: string } }).dataset.key}):`, {
// // element: g,
// // pathCount: paths.length,
// // paths: Array.from(paths).map(p => ({
// // className: p.className.baseVal,
// // d: p.getAttribute('d')?.substring(0, 50) + '...'
// // }))
// // });
// });
function showForWithLabel(g: SVGGElement) {
// console.log("showForWithLabel called for:", g);
showFor(g);
const idx = gs.indexOf(g);
labels.forEach((l) => l.classList.remove("active"));
if (idx >= 0) labels[idx]!.classList.add("active");
}
// 3) 라벨 DOM 생성
labels = [];
if (labelsLayerRef.current) {
labelsLayerRef.current.innerHTML = "";
gs.forEach((g) => {
const el = document.createElement("div");
el.className = styles.basinLabel || '';
el.textContent = (g as SVGGElement & { dataset: { label: string } }).dataset.label || '';
labelsLayerRef.current!.appendChild(el);
labels.push(el);
});
}
// 3) 라벨 위치
const positionLabels = () => {
if (!stageRef.current || !svg) return;
const sRect = stageRef.current.getBoundingClientRect();
const inset =
parseFloat(
getComputedStyle(stageRef.current).getPropertyValue(
"--map-inset",
) || "0",
) / 100;
const left = sRect.width * inset,
top = sRect.height * inset;
const w = sRect.width * (1 - inset * 2),
h = sRect.height * (1 - inset * 2);
const [vx, vy, vw, vh] = (
svg!.getAttribute("viewBox") || "0 0 100 100"
)
.split(/\s+/)
.map(Number);
const svgRect = svg.getBoundingClientRect();
const stageRect = stageRef.current.getBoundingClientRect();
const offsetX = svgRect.left - stageRect.left - left;
const offsetY = svgRect.top - stageRect.top - top;
gs.forEach((g, i) => {
const key = (g as SVGGElement & { dataset: { key: string } }).dataset.key;
const manual = (MANUAL_LABEL_POS as Record<string, { x?: number; y?: number; px?: number; py?: number }>)[key];
let px, py;
if (
manual &&
(("x" in manual && "y" in manual) ||
("px" in manual && "py" in manual))
) {
if ("px" in manual) {
px = left + (manual.px || 0) + offsetX;
py = top + (manual.py || 0) + offsetY;
} else {
px = left + w * (manual.x || 0) + offsetX;
py = top + h * (manual.y || 0) + offsetY;
}
} else {
const b = g.getBBox();
const cx = b.x + b.width / 2,
cy = b.y + b.height / 2;
px = ((cx - vx!) / vw!) * w + left + offsetX;
py = ((cy - vy!) / vh!) * h + top + offsetY;
}
labels[i]!.style.left = `${px}px`;
labels[i]!.style.top = `${py + 18}px`;
});
}
// 4) 상호작용 (함수들은 이미 위에서 정의됨)
gs.forEach((g) => {
g.addEventListener("mouseenter", () => {
clear();
g.classList.add("active");
showForWithLabel(g);
});
g.addEventListener("mousemove", () => {
if (g.classList.contains("active")) showForWithLabel(g);
});
g.addEventListener("mouseleave", clear);
g.addEventListener("click", (e) => {
e.stopPropagation();
clear();
g.classList.add("active");
showForWithLabel(g);
const basinKey = g.dataset.key as keyof typeof BASIN_DATA | undefined;
if (basinKey) {
const basinName = BASIN_DATA[basinKey].name;
setSelectedBasin(basinName);
}
});
});
if (stageRef.current) {
stageRef.current.addEventListener("mouseleave", clear);
stageRef.current.addEventListener("click", () => clear());
}
// 5) 실행
positionLabels();
window.addEventListener("resize", positionLabels);
new window.ResizeObserver(positionLabels).observe(stageRef.current!);
})
.catch((error) => {
console.error("Error loading SVG:", error);
if (svgLayerRef.current)
svgLayerRef.current.innerHTML =
`<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #666; font-size: 14px;">지도를 불러올 수 없습니다. (${error.message})</div>`;
});
// cleanup
return () => {
window.removeEventListener("resize", () => {});
};
}, [selectedBasinKey, showFor]); // 의존성 배열에 mapOperFclt 추가 불필요
const handleSvcInfoClick = (clickedSvcId: string) => {
setServices(prevServices =>
prevServices.map(svc => ({
...svc,
active: svc.svcId === clickedSvcId,
}))
);
};
// 스타일은 필요에 따라 tailwind, css 등으로 커스터마이즈하세요.
return (
<div className="col-span-3 !p-0 relative z-10" style={{ height: "765px", background: "rgba(255, 255, 255, 0.6)" }}>
<div className="card card-border" style={{ height: "100%" }}>
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">유역별 정보</h3>
<div className="flex gap-2">
{services.map((svc) => (
<Button
key={svc.name}
className="h-[3.2rem] float-right"
color={svc.active ? "primary" : "white"}
onClick={() => handleSvcInfoClick(svc.svcId)}
>
{svc.name}
</Button>
))}
</div>
</div>
<div id="mapArea" style={{height: "calc(100% - 40px)"}}>
<div
ref={stageRef}
id="stage"
style={
{
aspectRatio: 0.72/1,
width: "100%",
maxHeight: "100%",
position: "relative",
margin: "auto",
"--map-inset": "0%",
} as React.CSSProperties
}
>
<div
className={styles.bgLayer}
id="bgLayer"
/>
<div
ref={svgLayerRef}
id="svgLayer"
className={`${styles.svgLayer} ${styles.svgContainer}`}
style={{ width: "100%", height: "100%" }}
/>
<div
ref={labelsLayerRef}
id="labelsLayer"
className={styles.labelsLayer}
style={{
position: "absolute",
left: 0,
top: 0,
width: "100%",
height: "100%",
pointerEvents: "none",
}}
/>
<div
ref={popupRef}
id="popup"
className={`${styles.popup} absolute z-20 font-sans border border-blue-200 rounded-lg shadow-md overflow-hidden opacity-0 invisible bg-blue-50`}
style={{
minWidth: 160,
display: 'grid',
gridTemplateColumns: `repeat(${Math.max(1, popupContent.items.length)}, 1fr)`,
}}
>
{/* 제목 영역 */}
<div className="bg-blue-100 text-center font-bold border-b-2 border-blue-200 flex items-center justify-center" style={{ gridColumn: `span ${Math.max(1, popupContent.items.length)}` }}>
{popupContent.title}
</div>
{/* 헤더 및 데이터 동적 렌더링 */}
{popupContent.items.map((item, index) => (
<div key={`header-${index}`} className={`text-center border-b-2 border-blue-200 flex items-center justify-center ${index < popupContent.items.length - 1 ? 'border-r-2' : ''}`}>
{item.header}
</div>
))}
{popupContent.items.map((item, index) => (
<div key={`data-${index}`} className={`text-center flex items-center justify-center ${index < popupContent.items.length - 1 ? 'border-r-2' : ''}`}>
{item.data}
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
}
@@ -1,32 +0,0 @@
export interface SiteStatisticsContent {
siteId: string;
siteNm: string;
accessHour: string;
accessCnt: number;
}
export interface MenuStatisticsContent {
menuCd: number;
menuNm: string;
accessCnt: number;
}
export interface Dashboard {
memberTotCnt: number;
memberTodayCnt: number;
pstTotCnt: number;
pstTodayCnt: number;
accessTodayCnt: number;
accessBefDayPer: number;
apiTodayCnt: number;
waitMemberCnt: number;
waitCntnCnt: number;
notiList: [];
schdlList: [];
siteAccessList: SiteStatisticsContent[];
menuAccessList: MenuStatisticsContent[];
}
export interface DashboardService {
getDashboard(): Promise<Dashboard>;
}
@@ -0,0 +1,77 @@
export interface OperFcltContent {
rvbsn: string; // 유역
cnt: number; // 개소수
fctCpc: number; // 가동률 (용량)
gubun: string; // 구분
}
export interface OperWqltContent {
rvbsn: string; // 유역명
yr: string; // 연도
sumInflBod: number; // 유입수 BOD 합계
sumInflToc: number; // 유입수 TOC 합계
sumInflSs: number; // 유입수 SS 합계
sumInflTp: number; // 유입수 TP 합계
sumInflTn: number; // 유입수 TN 합계
sumEfflBod: number; // 유출수 BOD 합계
sumEfflToc: number; // 유출수 TOC 합계
sumEfflSs: number; // 유출수 SS 합계
sumEfflTp: number; // 유출수 TP 합계
sumEfflTn: number; // 유출수 TN 합계
avgInflBod: number; // 기간 평균 유입수 BOD
avgInflToc: number; // 기간 평균 유입수 TOC
avgInflSs: number; // 기간 평균 유입수 SS
avgInflTp: number; // 기간 평균 유입수 TP
avgInflTn: number; // 기간 평균 유입수 TN
avgEfflBod: number; // 기간 평균 유출수 BOD
avgEfflToc: number; // 기간 평균 유출수 TOC
avgEfflSs: number; // 기간 평균 유출수 SS
avgEfflTp: number; // 기간 평균 유출수 TP
avgEfflTn: number; // 기간 평균 유출수 TN
}
export interface ReuseMapContent {
name: string; // 유역명
reuseAmnt: number; // 하수재이용량
cnt: number; // 시설수
}
export interface ReuseStatusContent {
yr: string;
sewage: number;
reclaimed: number;
rainwater: number;
}
export interface OperationService {
getStandardYear(): Promise<string> ;
getDashOperCntRt(year: string): Promise<OperFcltContent[]>;
getDashOperWqlt(year: string): Promise<OperWqltContent[]>;
getReuseStatus(): Promise<ReuseStatusContent[]>;
}
export type mapOperFcltItem = {
name: string;
cnt: number;
rt: number;
};
export type mapOperFcltType = {
colTitle: {
cnt: string; // 개소수
rt: string; // 가동률
};
data: mapOperFcltItem[];
};
export type mapReuseMapType = {
colTitle: {
reuseAmnt: string; // 하수재이용량
cnt: string; // 시설수
};
data: ReuseMapContent[];
};
@@ -0,0 +1,65 @@
export const basinKeyMap: { [key: string]: string } = {
"한강": "han",
"금강": "geum",
"낙동강": "nakdong",
"영산강": "yeongsan",
};
// --- 라벨 좌표(수동 고정값) : inset 적용 영역 기준 퍼센트 또는 px 사용 ---
// - 퍼센트: { x:0~1, y:0~1 } (권장: 반응형에 유리)
// - 픽셀: { px:number, py:number } (inset 좌상단 원점 기준)
export const MANUAL_LABEL_POS = {
han: { x: 0.52, y: 0.14 },
geum: { x: 0.28, y: 0.47 },
nakdong: { x: 0.64, y: 0.47 },
yeongsan: { x: 0.18, y: 0.77 },
};
export const BASIN_DATA = {
han: {
name: "한강",
colTitle: "시설수",
rows: [
["500m³ 미만", 100000000],
["500m³ 이상", 100000000],
],
},
geum: {
name: "금강",
colTitle: "시설수",
rows: [
["500m³ 미만", 100000000],
["500m³ 이상", 100000000],
],
},
nakdong: {
name: "낙동강",
colTitle: "시설수",
rows: [
["500m³ 미만", 100000000],
["500m³ 이상", 100000000],
],
},
yeongsan: {
name: "영산강",
colTitle: "시설수",
rows: [
["500m³ 미만", 100000000],
["500m³ 이상", 100000000],
],
},
};
export const BASIN_IDS = [
{ key: "han", labels: ["한강", "han"] },
{ key: "geum", labels: ["금강", "geum"] },
{ key: "nakdong", labels: ["낙동강", "nakdong"] },
{ key: "yeongsan", labels: ["영산강", "yeongsan"] },
];
export const svcInfo = [
{svcId: "wr", active: true, name:"물재이용"},
{svcId: "sn", active: false, name:"하수관로"},
{svcId: "fo", active: false, name:"운영실태"},
]
@@ -0,0 +1,85 @@
import { basinKeyMap } from "../model/map-data";
export interface PplLengthContent {
zoneNm: string;
sumPrlgVl: number;
usgCd?: string;
yearGroup?: string;
}
export interface SewerageService {
getPplLength(ymdFrom: string, ymdTo: string, useCd: string): Promise<PplLengthContent[]>;
getPplUseLength(ymdFrom: string, ymdTo: string, useCd: string): Promise<PplLengthContent[]>;
getMvUsgPplLength(): Promise<PplLengthContent[]>;
getMvOldPplLength(): Promise<PplLengthContent[]>;
}
export interface ChartDataSet {
label: string;
data: (number | undefined)[];
backgroundColor: string;
borderRadius?: number;
}
export interface OldPplChartData {
labels: string[];
datasets: ChartDataSet[];
}
type SnOldPplItem = {
name: string;
colTitle: string[];
rows: number[];
};
type SnOldPplLength = {
[key: string]: SnOldPplItem;
};
export type mapPplOldItem = {
name: string;
totalLen: number;
pplLen2025: number;
pplLen2530: number;
pplLen30: number;
};
export type mapPplOldType = {
colTitle: {
totalLen: string; // 총길이
pplLen2025: string; // 20~25년
pplLen2530: string; // 25~30년
pplLen30: string; // 30년 이상
};
data: mapPplOldItem[];
};
export function buildSnOldPplLength(
chartData: {
labels: string[];
datasets: { label: string; data: number[] }[];
},
totalLengths: number[]
): SnOldPplLength {
const colTitle = ["총길이", ...chartData.datasets.map((ds) => ds.label)];
const result: SnOldPplLength = {};
chartData.labels.forEach((basinName, index) => {
const key = basinKeyMap[basinName]; // geum, han, nakdong, yeongsan
if (!key) return; // 매핑 안되면 스킵
const total = totalLengths[index] ?? 0;
const datasetValues = chartData.datasets.map(
(ds) => ds.data[index] ?? 0,
);
result[key] = {
name: basinName,
colTitle,
rows: [
total, // 총길이 (undefined면 0)
...datasetValues, // 각 연차 데이터 (undefined면 0)
],
};
});
return result;
}
@@ -0,0 +1,49 @@
import type { ColumnDef } from "@tanstack/react-table";
import type { Support } from "../api/support-service";
const typeLabelMap: Record<string, string> = {
waterQuality: "수질",
flood: "침수",
technical: "기술지원",
};
const supportTableColumns: ColumnDef<Support>[] = [
{
accessorKey: "type",
header: "구분",
cell: ({ getValue }) => {
const value = getValue() as string;
return typeLabelMap[value] || value;
},
},
{
accessorKey: "total",
header: "합계",
},
{
accessorKey: "han",
header: "한강",
},
{
accessorKey: "nakdong",
header: "낙동강",
},
{
accessorKey: "geum",
header: "금강",
},
{
accessorKey: "youngsang",
header: "영산강",
},
{
accessorKey: "ongoing",
header: "진행중",
},
{
accessorKey: "completed",
header: "완료",
},
];
export { supportTableColumns };
@@ -1,28 +1,9 @@
import dayjs from "dayjs";
import type { ColumnDef } from "@tanstack/react-table";
import { Badge } from "@common/components";
import type { Post, MyPost } from "@/entities/post";
import type { MyPost, NoticePost } from "@/entities/post";
import type { Schedule } from "../api/schedule-service";
const noticePostTableColumns: ColumnDef<Post>[] = [
{
header: "번호",
cell: ({ row, table }) => {
if (row.original.upndFixYn === "Y") {
return (
<Badge
color="orange"
className="flex size-8 items-center justify-center"
>
<i className="diveicon di-volume-up-fill" />
</Badge>
);
}
const totalRows = table.getRowCount();
const currentPage = table.getState().pagination.pageIndex;
const pageSize = table.getState().pagination.pageSize;
return totalRows - (currentPage * pageSize + row.index);
},
},
const noticePostTableColumns: ColumnDef<NoticePost>[] = [
{
accessorKey: "pstTtl",
header: "제목",
@@ -43,33 +24,35 @@ const noticePostTableColumns: ColumnDef<Post>[] = [
];
const myPostTableColumns: ColumnDef<MyPost>[] = [
{
header: "번호",
cell: ({ row, table }) => {
const totalRows = table.getRowCount();
const currentPage = table.getState().pagination.pageIndex;
const pageSize = table.getState().pagination.pageSize;
return totalRows - (currentPage * pageSize + row.index);
},
},
{
accessorKey: "pstTtl",
header: "제목",
},
{
accessorKey: "wrtrNm",
header: "작성자",
},
{
accessorKey: "regDt",
header: "작성일",
cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"),
},
];
const scheduleTableColumns: ColumnDef<Schedule>[] = [
{
accessorKey: "inqNocs",
header: "조회수",
accessorKey: "schdlTtl",
header: "주요일정",
cell: ({ row }) => {
const startDate = dayjs(row.original.schdlBgngDt).format("YYYY-MM-DD");
const endDate = dayjs(row.original.schdlEndDt).format("YYYY-MM-DD");
const schdlTtl = row.original.schdlTtl;
return (
<div className="text-left">
[{startDate} ~ {endDate}]<br />{schdlTtl}
</div>
);
},
},
];
export { noticePostTableColumns, myPostTableColumns };
export { noticePostTableColumns, myPostTableColumns, scheduleTableColumns };
@@ -0,0 +1,186 @@
"use client";
import { useRef } from "react";
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import { getImageUrlByAtflCd, getBannerList, type Banner } from "../api/banner-service";
import { getCookie } from "@/shared/lib";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
Autoplay,
} from "@common/components";
type BannerImageProps = {
atflCd: string;
lnkgUrlAddr: string;
};
const BannerList: React.FC = () => {
const siteId = getCookie("site-id-key") ?? "";
const BannerImage = ({ atflCd, lnkgUrlAddr }: BannerImageProps) => {
const { data: imageUrl, isLoading } = useQuery({
queryKey: [ENDPOINT.ATTACH_SERVICE.IMAGES, atflCd],
queryFn: () => getImageUrlByAtflCd(atflCd),
enabled: !!atflCd,
});
const handleClick = () => {
if (lnkgUrlAddr) {
window.open(lnkgUrlAddr, "_blank");
}
};
if (isLoading) {
return (
<div className="relative flex h-[210px] w-full items-center justify-center bg-accent">
<div className="text-sm text-muted-foreground">로딩 중...</div>
</div>
);
}
if (!imageUrl) {
return (
<div className="relative flex h-[210px] w-full items-center justify-center bg-accent">
<div className="text-sm text-muted-foreground">이미지 없음</div>
</div>
);
}
return (
<div
className="relative h-[210px] w-full cursor-pointer overflow-hidden transition-opacity hover:opacity-80"
onClick={handleClick}
>
<div
className="h-full w-full bg-cover bg-center"
style={{ backgroundImage: `url(${imageUrl})` }}
role="img"
aria-label="배너 이미지"
/>
</div>
);
};
const {
data: bannerList,
error: bannerError,
isLoading: isLoadingBanners,
} = useQuery<Banner[]>({
queryKey: [ENDPOINT.CMS_SERVICE.BANNERS, siteId],
queryFn: () => getBannerList(siteId),
enabled: !!siteId,
retry: false,
});
const plugin = useRef(Autoplay({ delay: 2000, stopOnInteraction: true }));
// MAIN 타입 배너만 필터링
const mainBanners =
bannerList?.filter((banner) => banner.bnrTypeCd === "BNRMAIN") ?? [];
// 에러 상태 판별 헬퍼 함수
const getErrorStatus = (e: unknown): number | undefined => {
if (!e || typeof e !== "object") return undefined;
const maybe = e as { status?: number; response?: { status?: number } };
return maybe.status ?? maybe.response?.status;
};
// 401 에러 처리
if (bannerError) {
const is401Error =
(bannerError instanceof Error && bannerError.message.includes("401")) ||
getErrorStatus(bannerError) === 401;
if (is401Error) {
console.error("[BannerList] 401 인증 오류:", bannerError);
return (
<div className="card card-border">
<div className="relative flex h-[280px] w-full items-center justify-center bg-accent">
<div className="text-sm text-red-500">
인증이 필요합니다. 로그인해주세요.
</div>
</div>
</div>
);
}
console.error("[BannerList] 배너 조회 오류:", bannerError);
return (
<div className="card card-border">
<div className="relative flex h-[240px] w-full items-center justify-center bg-accent">
<div className="text-sm text-muted-foreground">
배너를 불러올 수 없습니다
</div>
</div>
</div>
);
}
// 로딩 중
if (isLoadingBanners) {
return (
<div className="card card-border">
<div className="relative flex h-[240px] w-full items-center justify-center bg-accent">
<div className="text-sm text-muted-foreground">배너 로딩 중...</div>
</div>
</div>
);
}
// siteId가 없을 때
if (!siteId) {
return (
<div className="card card-border">
<div className="relative flex h-[240px] w-full items-center justify-center bg-accent">
<div className="text-sm text-muted-foreground">
사이트 ID가 없습니다
</div>
</div>
</div>
);
}
return (
<div className="card card-border" style={{ height: "230px" }}>
<Carousel
plugins={[plugin.current]}
opts={{ loop: true }}
className="relative"
>
<CarouselContent className="-ml-0">
{mainBanners.length > 0 ? (
mainBanners.map((banner) => (
<CarouselItem
key={`banner-${banner.bnrSn}`}
className="basis-full pl-0"
>
<BannerImage
atflCd={banner.atflCd}
lnkgUrlAddr={banner.lnkgUrlAddr}
/>
</CarouselItem>
))
) : (
<CarouselItem className="basis-full pl-0">
<div className="relative flex h-[210px] w-full items-center justify-center bg-accent">
<div className="text-sm text-muted-foreground">
배너가 없습니다
</div>
</div>
</CarouselItem>
)}
</CarouselContent>
<CarouselPrevious className="absolute left-2 top-1/2 z-10 !bg-[rgba(255,255,255,0.8)] text-accent-foreground" />
<CarouselNext className="absolute right-2 top-1/2 z-10 !bg-[rgba(255,255,255,0.8)] text-accent-foreground" />
</Carousel>
</div>
);
};
export default BannerList;
@@ -0,0 +1,42 @@
"use client";
import React from "react";
import {
cn,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@common/components";
interface BasinSelectProps {
selectedBasin: string;
onValueChange: (value: string) => void;
className?: string;
}
const BasinSelect: React.FC<BasinSelectProps> = ({
selectedBasin,
onValueChange,
className
}) => {
const basins = ["한강", "금강", "낙동강", "영산강"];
return (
<Select value={selectedBasin} onValueChange={onValueChange}>
<SelectTrigger className={cn("w-[12rem]", className)}>
<SelectValue placeholder="유역 선택" />
</SelectTrigger>
<SelectContent>
{basins.map((basin) => (
<SelectItem key={basin} value={basin}>
{basin}
</SelectItem>
))}
</SelectContent>
</Select>
);
};
export default BasinSelect;
+74 -164
View File
@@ -1,10 +1,11 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import Image from "next/image";
import { tokenManager } from "@/entities/auth/lib/token-manager";
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import { KoreaBasinMap } from "../lib/KoreaBasinMap";
import { KoreaBasinMap } from "../lib/korea-basin-map";
import {
Chart as ChartJS,
CategoryScale,
@@ -17,7 +18,18 @@ import {
Tooltip,
ArcElement,
} from "chart.js";
import { Doughnut } from "react-chartjs-2";
import SnPplStatus from "./sn-ppl-status";
import SnOldPpl from "./sn-old-ppl";
import FoFcltCntRt from "./fo-fclt-cnt-rt";
import FoWqltInflEffl from "./fo-wqlt-Infl-effl";
import ReuseChart from "./reuse-chart";
import ReuseStatus from "./reuse-status";
import NoticeList from "./notice-list";
import BannerList from "./banner-list";
import SupportList from "./support-list";
import DataStatusList from "./datastatus-list";
import MyPostList from "./mypost-list";
import ScheduleList from "./schedule-list";
// Chart.js 등록
ChartJS.register(
@@ -42,8 +54,7 @@ interface UserInfo {
function DashboardPage() {
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
const [error, setError] = useState("");
const router = useRouter();
const router = useRouter();
useEffect(() => {
const fetchUserInfo = async () => {
console.log("fetchUserInfo called");
@@ -102,197 +113,96 @@ function DashboardPage() {
};
return (
<div className="p-6">
<div className="p-2 sm:p-4 md:p-6">
<div className="flex flex-col gap-1">
{/* 첫번째 라인 */}
<div className="grid grid-cols-8 gap-1">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-8 gap-1">
{/* 첫번쨰 열 */}
<div className="col-span-2 h-250 !p-0">
{/* 전국 하수처리시설 가동률 */}
<div className="card card-border">
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">전국 하수처리시설 가동률</h3>
</div>
<div className="h-60 p-4">
<Doughnut
data={{
datasets: [{
data: [75, 25], // 75% 가동률
backgroundColor: ['#00DFE5', '#f5f5f5'],
borderWidth: 0
}]
}}
options={{
responsive: true,
maintainAspectRatio: false,
cutout: '70%',
circumference: 180, // 반원
rotation: -90, // 왼쪽에서 시작
plugins: {
legend: {
display: false
},
tooltip: {
enabled: false
}
}
}}
plugins={[{
id: 'centerText',
afterDraw: (chart) => {
const ctx = chart.ctx;
const centerX = chart.width / 2;
const centerY = chart.height / 2;
// 가동률 텍스트
ctx.save();
ctx.font = 'bold 24px Arial';
ctx.fillStyle = '#00DFE5';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('75%', centerX, centerY + 10);
// 라벨 텍스트
ctx.font = '12px Arial';
ctx.fillStyle = '#666';
ctx.fillText('가동률', centerX, centerY + 30);
ctx.restore();
}
}]}
/>
</div>
</div>
<div className="col-span-1 md:col-span-1 lg:col-span-1 xl:col-span-2 h-250 !p-0">
{/* 전국 하수처리시설 개소수 및 가동률 */}
<FoFcltCntRt />
{/* 오수관로 및 우수관로 현황 */}
<div className="card card-border">
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">오수관로 및 우수관로 현황</h3>
</div>
<div className="h-60 p-4"></div>
</div>
<SnPplStatus />
{/* 하수처리수 재이용 */}
<div className="card card-border">
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">하수처리수 재이용</h3>
</div>
<div className="h-60 p-4"></div>
</div>
{/* 하수처리수 재이용 (m³) */}
<ReuseChart />
</div>
{/* 두번쨰 열 */}
<div className="col-span-2 h-250 !p-0">
{/* 하수처리시설(개소) */}
<div className="card card-border">
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">하수처리시설(개소)</h3>
</div>
<div className="grid grid-cols-2 gap-1 h-60 !p-0">
<div className="card card-border">
<div className="card-header text-foreground text-center">
<h3 className="text-l font-medium" style={{textAlign: "center", width: "100%"}}>500㎥ 미만(개소)</h3>
</div>
<div style={{fontSize: "38px", textAlign: "center", fontWeight: "600", lineHeight: "1.2", color: "#0ec58a", padding: "2px 0"}}>3,567 <span style={{fontSize: "14px", color: "#000000"}}>개소</span></div>
</div>
<div className="card card-border">
<div className="card-header text-foreground text-center">
<h3 className="text-l font-medium" style={{textAlign: "center", width: "100%"}}>500㎥ 이상(개소)</h3>
</div>
<div style={{fontSize: "38px", textAlign: "center", fontWeight: "600", lineHeight: "1.2", color: "#0ec58a", padding: "2px 0"}}>767 <span style={{fontSize: "14px", color: "#000000"}}>개소</span></div>
</div>
<div className="col-span-2" style={{width: "100%", textAlign: "right"}}>기준년월 : 2025.09 </div>
</div>
</div>
{/* 노후하수관로현황 */}
<div className="card card-border">
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">노후하수관로현황</h3>
</div>
<div className="h-70 p-4">
</div>
</div>
<div className="col-span-1 md:col-span-1 lg:col-span-1 xl:col-span-2 h-250 !p-0">
{/* 수계별 유입ㆍ방류수질 분석 */}
<FoWqltInflEffl />
{/* 재이용시설(개소) */}
<div className="card card-border">
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">재이용시설(개소)</h3>
</div>
<div className="h-70 p-4">
</div>
</div>
{/* 노후하수관로현황 */}
<SnOldPpl />
{/* 물재이용 현황 (재이용량, m³) */}
<ReuseStatus />
</div>
{/* 지도영역 */}
<div className="col-span-3 h-200 !p-0 relative z-10">
<div className="card card-border">
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">유역별 정보</h3>
</div>
<KoreaBasinMap />
</div>
<div className="col-span-1 md:col-span-2 lg:col-span-1 xl:col-span-3">
<KoreaBasinMap />
</div>
{/* 사용자 정보 카드 */}
<div className="card card-border col-span-1 h-80 p-10">
<h2 className="mb-6 text-2xl">사용자 정보</h2>
<div className="col-span-1 md:col-span-2 lg:col-span-1 xl:col-span-1 !p-0">
{error && <div className="mb-4 text-red-500">{error}</div>}
{userInfo ? (
<div className="mb-4">
<div>
<b>아이디:</b> {userInfo.userId}
<div className="card card_border login_info_box" style={{ minHeight: "150px", height: "auto" }}>
<div className="login_header" style={{ paddingLeft: "10px", paddingRight: "10px" }}>
<div className="login_avatar">
<Image
src="/images/icon_loginID.png"
alt="프로필"
width={40}
height={40}
onError={(e) => {
const target = e.target as HTMLImageElement;
target.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'%3E%3Ccircle cx='20' cy='20' r='18' fill='%23e1e5e9'/%3E%3Ctext x='20' y='26' font-size='20' text-anchor='middle' fill='%23999'%3E%3F%3C/text%3E%3C/svg%3E";
}}
/>
</div>
<div>
<b>이메일:</b> {userInfo.email || userInfo.userId}
</div>
<div>
<b>이름:</b> {userInfo.userName || "-"}
</div>
<div>
<b>2FA 활성화:</b>{" "}
{userInfo.twoFactorEnabled ? "예" : "아니오"}
<div className="login_user_info">
<div className="user_name">{userInfo.userName || "-"}</div>
<div className="user_status">온라인</div>
<div className="user_status text-xs sm:text-sm">
<b>2FA 활성화:</b>{" "}
{userInfo.twoFactorEnabled ? "예" : "아니오"}
</div>
</div>
</div>
<div className="login_actions" style={{ paddingLeft: "10px", paddingRight: "10px" }}>
<button className="login_btn text-xs sm:text-sm" onClick={handleLogout}>로그아웃</button>
<button className="signup_btn text-xs sm:text-sm" onClick={handleChangePassword}>마이페이지</button>
</div>
</div>
) : (
!error && <div>사용자 정보를 불러오는 중...</div>
)}
<button className="btn mb-2 w-full" onClick={handleLogout}>
로그아웃
</button>
<button className="btn w-full" onClick={handleChangePassword}>
패스워드 변경
</button>
</div>
<ScheduleList />
<MyPostList />
</div>
</div>
{/* 두번째 라인 */}
<div className="grid grid-cols-4 gap-1">
<div className="card card-border"id="grouping-statistics">
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">지원현황</h3>
</div>
<div className="h-70 p-4">
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-1">
<div className="card card-border" style={{ height: "250px" }}>
<SupportList />
</div>
<div className="card card-border">
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">자료관 자료현황</h3>
</div>
<div className="h-70 p-4">
</div>
<div className="card card-border" style={{ height: "250px" }}>
<DataStatusList />
</div>
<div className="card card-border">
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">공지사항</h3>
</div>
<div className="h-70 p-4">
</div>
<div className="card card-border" style={{ height: "250px" }}>
<NoticeList />
</div>
{/* 배너 영역 */}
<div className="card card-border">
<div className="card card-border" style={{ height: "250px" }}>
<BannerList />
</div>
</div>
</div>
@@ -0,0 +1,174 @@
"use client";
import { useRef } from "react";
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import { getDataStatusList, type DataStatus } from "../api/datastatus-service";
import type { PageableList } from "@/shared/model";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
Autoplay,
} from "@common/components";
// 지역 타입에 따른 아이콘과 클래스 결정
const getRegionInfo = (regionName: string) => {
if (regionName.includes("서울특별시")) {
return { icon: "🏛️", className: "special-city" };
}
if (
regionName.includes("광역시") ||
regionName.includes("부산") ||
regionName.includes("대구") ||
regionName.includes("인천") ||
regionName.includes("광주") ||
regionName.includes("대전") ||
regionName.includes("울산")
) {
return { icon: "🏭", className: "metropolitan-city" };
}
return { icon: "🌊", className: "province" };
};
const DataStatusList: React.FC = () => {
const plugin = useRef(
Autoplay({ delay: 3000, stopOnInteraction: true })
);
const {
data: dataStatusResponse,
error: dataStatusError,
isLoading: isLoadingDataStatus,
} = useQuery<PageableList<DataStatus>>({
queryKey: [ENDPOINT.DC_SERVICE.DATA_STATUS],
queryFn: () => getDataStatusList(),
enabled: true,
retry: false,
staleTime: 5 * 60 * 1000, // 5분간 fresh 상태 유지
refetchOnWindowFocus: false, // 윈도우 포커스 시 리페칭 방지
refetchOnMount: false, // 마운트 시 리페칭 방지 (캐시된 데이터 사용)
});
const dataStatusList = dataStatusResponse?.content || [];
// 에러 상태 판별 헬퍼 함수
const getErrorStatus = (e: unknown): number | undefined => {
if (!e || typeof e !== "object") return undefined;
const maybe = e as { status?: number; response?: { status?: number } };
return maybe.status ?? maybe.response?.status;
};
// 401 에러 처리
if (dataStatusError) {
const is401Error =
(dataStatusError instanceof Error && dataStatusError.message.includes("401")) ||
getErrorStatus(dataStatusError) === 401;
if (is401Error) {
console.error("[DataStatusList] 401 인증 오류:", dataStatusError);
return (
<div className="card card-border">
<div className="relative flex h-[280px] w-full items-center justify-center bg-accent">
<div className="text-sm text-red-500">
인증이 필요합니다. 로그인해주세요.
</div>
</div>
</div>
);
}
console.error("[DataStatusList] 데이터 조회 오류:", dataStatusError);
return (
<div className="card card-border">
<div className="relative flex h-[240px] w-full items-center justify-center bg-accent">
<div className="text-sm text-muted-foreground">
데이터를 불러올 수 없습니다
</div>
</div>
</div>
);
}
// 로딩 중
if (isLoadingDataStatus) {
return (
<div className="card card-border">
<div className="relative flex h-[200px] w-full items-center justify-center bg-accent" >
<div className="text-sm text-muted-foreground">데이터 로딩 중...</div>
</div>
</div>
);
}
return (
<>
<div className="card_title bottomline">
<div className="leftArea">
<h4>자료현황</h4>
</div>
</div>
<div className="card card_paddingnone">
<div className="mid_info_area" style={{ width: "100%", padding: "5px" }}>
<Carousel
plugins={[plugin.current]}
opts={{ loop: true }}
className="relative mi_list_swiper"
>
<CarouselContent className="-ml-4 mi_list">
{dataStatusList.length > 0 ? (
dataStatusList.map((dataStatus) => {
const regionInfo = getRegionInfo(dataStatus.ctpvnm);
return (
<CarouselItem
key={`dataStatus-${dataStatus.ctpvnm}`}
className={`basis-1/2 pl-4 ml_box ${regionInfo.className}`}
style={{ width: "200px" }}
>
<div className="mb_section">
<div className="tit">
<i className="cloud-arrow-down-fill color_3">
{regionInfo.icon}
</i>
<span className="tt">{dataStatus.ctpvnm}</span>
</div>
<div className="info_row">
<div className="info_item">
<div className="info_label">기본계획</div>
<div className="info_value">
{dataStatus.ctpvgnsvycnt}
</div>
</div>
<div className="info_item">
<div className="info_label">파일수</div>
<div className="info_value">{dataStatus.filecnt}</div>
</div>
</div>
</div>
</CarouselItem>
);
})
) : (
<CarouselItem className="basis-full pl-0" style={{ height: "210px" }}>
<div className="relative flex h-[280px] w-full items-center justify-center bg-accent">
<div className="text-sm text-muted-foreground">
데이터가 없습니다
</div>
</div>
</CarouselItem>
)}
</CarouselContent>
<CarouselPrevious className="absolute left-2 top-1/2 z-10 !bg-[rgba(255,255,255,0.8)] text-accent-foreground" />
<CarouselNext className="absolute right-2 top-1/2 z-10 !bg-[rgba(255,255,255,0.8)] text-accent-foreground" />
</Carousel>
</div>
</div>
</>
);
};
export default DataStatusList;
@@ -0,0 +1,149 @@
"use client";
/**
* 수계별 시설운영 개소수 및 가동률
*/
import React, { useEffect, useState, useMemo } from 'react';
import { useStore } from "zustand";
import BasinSelect from "./basIn-select";
import { Doughnut } from "react-chartjs-2";
import type { Chart as ChartJS } from "chart.js";
// import type { mapOperFcltItem, mapBasinData } from "../model/fo-interface";
import { OperationApi, operFcltStore, basinStore } from "../api/fo-service";
const FoFcltCntRt: React.FC = () => {
const [year, setYear] = useState<string | null>(null);
const { selectedBasin, setSelectedBasin } = useStore(basinStore);
const operFcltStatus = useStore(operFcltStore, (state) => state.operFcltStatus);
const onBasinChange = (value: string) => {
setSelectedBasin(value);
};
useEffect(() => {
const fetchData = async () => {
try {
const standardYear = await OperationApi.getStandardYear();
setYear(standardYear);
await OperationApi.getDashOperCntRt(standardYear);
} catch (e) {
console.error("시설운영 데이터 조회 실패", e);
}
};
fetchData();
}, []);
const displayData = useMemo(() => {
if (!operFcltStatus) {
return { facilityCount: 0, operatingRate: 0, chartData: [0, 100] };
}
const basinData = operFcltStatus.filter(d => d.rvbsn === selectedBasin); // 선택된 유역의 데이터만 filter
const operatingData = basinData.find(d => d.gubun === 'R'); // 선택 유역의 R 데이터만
const nonOperatingData = basinData.find(d => d.gubun === 'A');
const facilityCount = operatingData?.cnt ?? 0;
const operatingCapacity = operatingData?.fctCpc ?? 0;
const nonOperatingCapacity = nonOperatingData?.fctCpc ?? 0;
const totalCapacity = operatingCapacity + nonOperatingCapacity;
const operatingRate = totalCapacity > 0 ? Math.round((operatingCapacity / totalCapacity) * 100) : 0;
const result = {
facilityCount,
operatingRate,
chartData: [operatingCapacity, nonOperatingCapacity],
};
// console.log(result);
return result;
}, [operFcltStatus, selectedBasin]);
return (
<div className="card card-border" style={{ height: "250px" }}>
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">수계별 시설운영 개소수 및 가동률</h3>
<BasinSelect selectedBasin={selectedBasin} onValueChange={onBasinChange} />
</div>
<div className="h-70 p-4">
<div className="flex gap-2 h-full">
{/* 왼쪽: 개소수 카드 */}
<div className="flex-1">
<div className="card card-border h-full flex flex-col">
<div className="card-header text-foreground text-center">
<h3 className="text-l font-medium mx-auto">개소수 ({year}년도)</h3>
</div>
<div className="flex items-center justify-center flex-1" style={{ height: "200px" }}>
<div style={{ fontSize: "38px", textAlign: "center", fontWeight: "600", lineHeight: "1.2", color: "#0ec58a" }}>
{displayData.facilityCount.toLocaleString()} <span style={{ fontSize: "14px", color: "#000000" }}>개소</span>
</div>
</div>
</div>
</div>
{/* 오른쪽: Doughnut 차트 */}
<div className="flex-1">
<div className="card card-border h-full flex flex-col">
<div className="card-header text-foreground text-center">
<h3 className="text-l font-medium mx-auto">가동률 ({year}년도)</h3>
</div>
<div className="flex-1 flex items-start justify-center">
<div style={{ width: "100px", height: "100px", display: "flex", alignItems: "flex-start", justifyContent: "center" }}>
<Doughnut
data={{
datasets: [{
data: displayData.chartData,
backgroundColor: ['#00DFE5', '#f5f5f5'],
borderWidth: 0
}]
}}
options={{
responsive: true,
maintainAspectRatio: true,
cutout: '60%',
circumference: 180, // 반원
rotation: -90, // 왼쪽에서 시작
layout: {
padding: 0,
},
plugins: {
legend: {
display: false
},
tooltip: {
enabled: false
}
}
}}
key={displayData.operatingRate} // Key를 변경하여 차트 강제 업데이트
plugins={[{
id: 'centerText',
afterDraw: (chart: ChartJS) => {
const ctx = chart.ctx;
const centerX = chart.width / 2;
const centerY = chart.height / 2;
const rate = displayData.operatingRate ?? 0;
// 가동률 텍스트
ctx.save();
ctx.font = 'bold 16px Arial';
ctx.fillStyle = '#00DFE5';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`${rate}%`, centerX, centerY + 10);
// 라벨 텍스트
ctx.font = '10px Arial';
ctx.fillStyle = '#666';
ctx.fillText('가동률', centerX, centerY + 28);
ctx.restore();
}
}]}
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default FoFcltCntRt;
@@ -0,0 +1,246 @@
"use client";
/**
* 수계별 유입ㆍ방류수질 분석
*/
import React, { useEffect, useState, useMemo } from 'react';
import { useStore } from "zustand";
import { Line } from 'react-chartjs-2';
import BasinSelect from "./basIn-select";
import {
Chart as ChartJS,
CategoryScale, LinearScale, PointElement, LineElement,
Title, Tooltip, Legend, type ChartOptions,
} from 'chart.js';
import type { OperWqltContent } from "../model/fo-interface";
import { OperationApi, basinStore } from "../api/fo-service";
import { defaultInitOperWqltState } from "../api/fo-service";
// Chart.js 등록
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
type WaterQualityType = 'BOD' | 'TOC' | 'SS' | 'TP' | 'TN';
const FoWqltInflEffl: React.FC = () => {
const { selectedBasin, setSelectedBasin } = useStore(basinStore);
const [selectedTab, setSelectedTab] = useState<WaterQualityType>('BOD');
const [wqltData, setWqltData] = useState<OperWqltContent[]>([]);
const tabs: WaterQualityType[] = ['BOD', 'TOC', 'SS', 'TP', 'TN'];
const onBasinChange = (value: string) => {
setSelectedBasin(value);
};
useEffect(() => {
setWqltData(defaultInitOperWqltState.operWqltStatus);
const fetchData = async () => {
try {
const standardYear = await OperationApi.getStandardYear();
const data = await OperationApi.getDashOperWqlt(standardYear);
setWqltData(data);
// console.log(wqltData);
} catch (e) {
console.error("시설 유입ㆍ방류수질 데이터 조회 실패", e);
}
};
fetchData();
}, []);
const chartData = useMemo(() => {
const filteredData = wqltData
.filter(d => d.rvbsn === selectedBasin) // 해당 유역
.sort((a, b) => Number(a.yr) - Number(b.yr));
const wqltKey = selectedTab.charAt(0).toUpperCase() + selectedTab.slice(1).toLowerCase();
const inflowKey = `sumInfl${wqltKey}` as keyof OperWqltContent;
const effluentKey = `sumEffl${wqltKey}` as keyof OperWqltContent;
const inflowAvgKey = `avgInfl${wqltKey}` as keyof OperWqltContent;
const effluentAvgKey = `avgEffl${wqltKey}` as keyof OperWqltContent;
const labels = filteredData.map(d => d.yr ?? '');
const inflowData = filteredData.map(d => d[inflowKey] as number);
const effluentData = filteredData.map(d => d[effluentKey] as number);
const inflowAvg = filteredData[0]?.[inflowAvgKey] as number ?? 0;
const effluentAvg = filteredData[0]?.[effluentAvgKey] as number ?? 0;
return { labels, inflowData, effluentData, inflowAvg, effluentAvg };
}, [wqltData, selectedBasin, selectedTab]);
// console.log(chartData);
// 차트 공통 옵션
// const getChartOptions = (yAxisLabel: string): ChartOptions<'line'> => ({
const getChartOptions = (): ChartOptions<'line'> => ({
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false,
},
plugins: {
legend: {
display: true,
position: 'bottom',
labels: {
usePointStyle: true,
boxWidth: 6,
color: '#000',
font: {
size: 8,
},
},
},
tooltip: {
enabled: true,
titleFont: { size: 10 },
bodyFont: { size: 10 },
},
},
scales: {
x: {
grid: {
color: 'rgba(255, 255, 255, 0.1)',
tickLength: 4, // 눈금 길이 축소
},
ticks: {
color: '#000',
font: {
size: 8, // 폰트 크기 축소
},
padding: 0,
},
},
y: {
grid: {
color: 'rgba(255, 255, 255, 0.1)',
},
ticks: {
color: '#000',
font: {
size: 8, // 폰트 크기 축소
},
maxTicksLimit: 4, // 눈금 개수 제한
padding: 2,
},
title: {
display: false,
},
},
},
layout: {
padding: {
top: 5,
bottom: 0,
left: 0,
right: 5
}
}
});
// 유입 데이터 차트 설정
const inflowChartData = {
labels: chartData.labels,
datasets: [
{
label: `유입${selectedTab} (mg/L)`,
data: chartData.inflowData,
borderColor: '#00D4FF',
backgroundColor: '#00D4FF',
borderWidth: 1,
pointRadius: 2,
pointHoverRadius: 4,
tension: 0.4,
},
{
label: '유입 평균 (mg/L)',
data: Array(chartData.labels.length).fill(chartData.inflowAvg),
borderColor: '#FF1493',
backgroundColor: '#FF1493',
borderWidth: 1,
borderDash: [10, 5],
pointRadius: 0,
pointHoverRadius: 4,
tension: 0,
},
],
};
// 방류 데이터 차트 설정
const effluentChartData = {
labels: chartData.labels,
datasets: [
{
label: `방류${selectedTab} (mg/L)`,
data: chartData.effluentData,
borderColor: '#00FF88',
backgroundColor: '#00FF88',
borderWidth: 1,
pointRadius: 2,
pointHoverRadius: 4,
tension: 0.4,
},
{
label: '방류 평균 (mg/L)',
data: Array(chartData.labels.length).fill(chartData.effluentAvg),
borderColor: '#FFB800',
backgroundColor: '#FFB800',
borderWidth: 1,
borderDash: [10, 5],
pointRadius: 0,
pointHoverRadius: 4,
tension: 0,
},
],
};
return (
<div className="card card-border" style={{ height: "250px" }}>
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">수계별 유입ㆍ방류수질 분석</h3>
<BasinSelect selectedBasin={selectedBasin} onValueChange={onBasinChange} />
</div>
<div className="h-70 p-0 flex items-center justify-center">
<div style={{ width: "100%", maxWidth: 400, height: 180 }}>
{/* 탭 메뉴 */}
<div style={{ display: 'flex', gap: '20px', borderBottom: '1px solid #000000', flexShrink: 0 }}>
{tabs.map((tab) => (
<button
key={tab}
onClick={() => setSelectedTab(tab)}
style={{
background: 'none',
border: 'none',
color: selectedTab === tab ? '#00D4FF' : '#6b7c93',
fontSize: '8px',
fontWeight: selectedTab === tab ? 'bold' : 'normal',
cursor: 'pointer',
borderBottom: selectedTab === tab ? '3px solid #00D4FF' : 'none',
transition: 'all 0.3s',
}}
>
{tab}
</button>
))}
</div>
{/* 차트 영역 */}
<div style={{ display: 'flex', flexDirection: 'row', flex: 1, minHeight: 0 }}>
{/* 유입 차트 */}
<div style={{ flex: 1, position: 'relative', minWidth: 0 }}>
<Line data={inflowChartData} options={getChartOptions()} />
</div>
{/* 방류 차트 */}
<div style={{ flex: 1, position: 'relative', minWidth: 0 }}>
<Line data={effluentChartData} options={getChartOptions()} />
</div>
</div>
</div>
</div>
</div>
);
};
export default FoWqltInflEffl;
@@ -0,0 +1,53 @@
"use client";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { getCoreRowModel } from "@tanstack/react-table";
import { PostApi, type MyPost, type NoticePost } from "@/entities/post";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import {
myPostTableColumns,
} from "../model/table-columns";
import { Button } from "@common/components";
const MyPostList: React.FC = () => {
const router = useRouter();
const myPostQuery = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.MY_POSTS],
queryFn: () =>
PostApi.getMyPostList({
page: 0,
size: 5,
}),
});
const handleMyPostRowClick = (row: MyPost) => {
router.push(`/cms/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`);
};
const { tableComponent: myPostTableComponent } = useDataTable({
query: myPostQuery,
tableOptions: {
columns: myPostTableColumns,
getCoreRowModel: getCoreRowModel(),
},
dataTableOptions: {
variant: "line",
onRowClick: handleMyPostRowClick
},
});
return (
<div className="card card-border !p-0" style={{ height: "300px" }}>
<div className="card-header !m-0 !border-none text-foreground">
<h3 className="text-2xl font-medium">내 게시글</h3>
</div>
{myPostTableComponent}
</div>
);
};
export default MyPostList;
@@ -0,0 +1,63 @@
"use client";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { getCoreRowModel } from "@tanstack/react-table";
import { PostApi, type NoticePost } from "@/entities/post";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import {
noticePostTableColumns,
} from "../model/table-columns";
import { Button } from "@common/components";
const NoticeList: React.FC = () => {
const router = useRouter();
const noticePostQuery = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS],
queryFn: () =>
PostApi.getNoticePostList({
page: 0,
size: 3,
keywordType: "bbsTypeCd",
keyword: "BBSNOTICE",
}),
});
const handleNoticePostRowClick = (row: NoticePost) => {
router.push(`/cms/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`);
};
const handleMoreClick = () => {
try {
router.push('/cms/bbsnotice/7');
} catch (error) {
console.error("공지사항 더보기 이동 실패", error);
}
};
const { tableComponent: noticePostTableComponent } = useDataTable({
query: noticePostQuery,
tableOptions: {
columns: noticePostTableColumns,
getCoreRowModel: getCoreRowModel(),
},
dataTableOptions: {
variant: "line",
onRowClick: handleNoticePostRowClick,
},
});
return (
<div className="card card-border !p-0" style={{ height: "220px" }}>
<div className="card-header !m-0 !border-none text-foreground">
<h3 className="text-2xl font-medium">공지사항</h3>
<Button className="h-[3.2rem]" onClick={handleMoreClick}>더보기</Button>
</div>
{noticePostTableComponent}
</div>
);
};
export default NoticeList;
@@ -0,0 +1,242 @@
"use client";
/**
* 물재이용 차트
*/
import React, { useState, useEffect, useRef } from 'react';
import { useStore } from "zustand";
import { Line } from 'react-chartjs-2';
import { Button } from "@common/components";
import type { ReuseStatusContent } from "../model/fo-interface";
import { OperationApi, reuseStore } from "../api/fo-service";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler,
type ChartOptions
} from 'chart.js';
// Chart.js 플러그인 등록
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler // 영역 채우기를 위해 필요
);
const ReuseChart: React.FC = () => {
const [activeIndex, setActiveIndex] = useState(0);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const reuseStatusData = useStore(
reuseStore,
(state) => state.reuseStatus) as ReuseStatusContent[];
useEffect(() => {
OperationApi.getReuseStatus().catch((e) => {
console.error("물재이용 차트 데이터 조회 실패", e);
});
}, []);
const labels = reuseStatusData?.map((r) => r.yr);
const sewageData = reuseStatusData?.map((r) => r.sewage);
const reclaimedData = reuseStatusData?.map((r) => r.reclaimed);
const rainwaterData = reuseStatusData?.map((r) => r.rainwater);
const commonChartData = {
datasets: [
{
label: '재이용량',
data: sewageData, // 기본은 하수처리수
borderColor: '#00D4FF', // 선 색상 (밝은 하늘색)
backgroundColor: 'rgba(0, 212, 255, 0.1)', // 채우기 색상 (투명도 있는 하늘색)
borderWidth: 3,
pointBackgroundColor: '#00D4FF',
pointBorderColor: '#fff',
pointBorderWidth: 2,
pointRadius: 6,
pointHoverRadius: 8,
fill: true, // 아래 영역 채우기 활성화
tension: 0.4, // 곡선 부드럽게
},
],
};
const commonChartOptions: ChartOptions<'line'> = {
maintainAspectRatio: false,
plugins: {
legend: {
display: false, // 범례 숨김 (이미지에 없음)
},
tooltip: {
callbacks: {
label: (context) => {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += new Intl.NumberFormat('ko-KR').format(context.parsed.y) + '㎥';
}
return label;
}
}
}
},
scales: {
x: {
grid: {
display: true,
color: '#f0f0f0', // 연한 회색 그리드
},
ticks: {
color: '#666',
}
},
y: {
min: 1100000, // Y축 최소값 설정 (그래프 모양을 위해)
grid: {
color: '#f0f0f0',
},
ticks: {
color: '#666',
callback: function(value) {
return new Intl.NumberFormat('ko-KR').format(Number(value)) + '㎥';
}
}
}
},
interaction: {
mode: 'index',
intersect: false,
},
};
const chartItems = [
{
title: "하수처리수 재이용량 (m³)",
data: {
...commonChartData,
labels,
datasets: [{ ...commonChartData.datasets[0], data: sewageData }],
},
options: commonChartOptions,
},
{
title: "중수도 재이용량 (m³)",
data: {
...commonChartData,
labels,
datasets: [{ ...commonChartData.datasets[0], data: reclaimedData }],
},
options: commonChartOptions,
},
{
title: "빗물 재이용량 (m³)",
data: {
...commonChartData,
labels,
datasets: [{ ...commonChartData.datasets[0], data: rainwaterData }],
},
options: commonChartOptions,
},
];
const startSlider = () => {
intervalRef.current = setInterval(() => {
setActiveIndex(prevIndex => (prevIndex + 1) % chartItems.length);
}, 7000);
};
const stopSlider = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
useEffect(() => {
startSlider();
return () => stopSlider();
}, []);
const handleIndicatorClick = (index: number) => {
stopSlider();
setActiveIndex(index);
startSlider();
};
const handleMoreClick = () => {
window.open(process.env.NEXT_PUBLIC_WR, "_blank", "noopener,noreferrer");
};
const viewportRef = useRef<HTMLDivElement>(null);
return (
<div className="card card-border" style={{ height: "250px" }}>
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">{chartItems[activeIndex]?.title}</h3>
<Button className="h-[3.2rem]" onClick={handleMoreClick}>더보기</Button>
</div>
<div className="relative" style={{ maxWidth: 400, height: 180, marginLeft: 20 }}>
<div className="h-full">
{/* ✅ 좌표 기준 (절대 이동 X) */}
<div
ref={viewportRef}
className="w-full overflow-hidden"
style={{ height: 170}}
>
{/* ✅ 이동 대상 */}
<div
className="flex h-full transition-transform duration-500 ease-in-out"
style={{
width: `${chartItems.length * 100}%`,
transform: `translateX(-${
activeIndex * (viewportRef.current?.offsetWidth ?? 0)
}px)`
}}
>
{chartItems.map((chart, index) => (
<div
key={index}
className="h-full flex-shrink-0"
style={{ width: `${100 / chartItems.length}%` }}
>
<Line
data={chart.data}
options={chart.options as ChartOptions<'line'>}
/>
</div>
))}
</div>
</div>
<div className="absolute left-1/2 -translate-x-1/2 flex space-x-2" style={{ bottom: '-1px'}}>
{chartItems.map((_, index) => (
<button
key={index}
onClick={() => handleIndicatorClick(index)}
className={`w-3 h-3 rounded-full cursor-pointer transition-colors ${
activeIndex === index ? 'bg-blue-500' : 'bg-gray-300'
}`}
aria-label={`Go to slide ${index + 1}`}
/>
))}
</div>
</div>
</div>
</div>
);
};
export default ReuseChart;
@@ -0,0 +1,85 @@
"use client";
/**
* 물재이용 현황
*/
import React, { useEffect } from 'react';
import { useStore } from "zustand";
import { Button } from "@common/components";
import type { ReuseStatusContent } from "../model/fo-interface";
import { OperationApi, reuseStore} from "../api/fo-service";
const ReuseStatus: React.FC = () => {
// const [reuseStatusData, setReuseStatusData] = useState<ReuseStatusContent[]>([]);
const reuseStatusData = useStore(
reuseStore,
(state) => state.reuseStatus ) as ReuseStatusContent[];
useEffect(() => {
OperationApi.getReuseStatus().catch((e) => {
console.error("물재이용 현황 데이터 조회 실패", e);
});
}, []);
// 숫자 포맷 함수 (천단위 쉼표)
const formatNumber = (num: number): string => {
return new Intl.NumberFormat('ko-KR').format(num);
};
const handleMoreClick = () => {
window.open(process.env.NEXT_PUBLIC_WR, "_blank", "noopener,noreferrer");
};
return (
<div className="card card-border" style={{ height: "250px" }}>
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">물재이용 현황 (재이용량, m³)</h3>
<Button className="h-[3.2rem]" onClick={handleMoreClick}>더보기</Button>
</div>
<div className="h-70 p-4">
<div className="w-full h-full flex items-center justify-center">
<table className="w-full border-collapse text-md">
<thead>
<tr style={{ height: "35px" }}>
<th className="bg-blue-500 text-white font-bold py-1 px-3 border border-gray-300">
년도
</th>
<th className="bg-blue-500 text-white font-bold py-1 px-3 border border-gray-300">
하수재이용량
</th>
<th className="bg-blue-500 text-white font-bold py-1 px-3 border border-gray-300">
중수도이용량
</th>
<th className="bg-blue-500 text-white font-bold py-1 px-3 border border-gray-300">
빗물이용량
</th>
</tr>
</thead>
<tbody>
{reuseStatusData && reuseStatusData.length > 0 && reuseStatusData?.map((row, index) => (
<tr key={index} className="hover:bg-gray-50" style={{ height: "35px" }}>
<td className="bg-blue-100 font-semibold text-center py-1 px-3 border border-gray-300">
{row.yr}
</td>
<td className="text-center py-1 px-3 border border-gray-300 bg-white">
{formatNumber(row.sewage)}
</td>
<td className="text-center py-1 px-3 border border-gray-300 bg-white">
{formatNumber(row.reclaimed)}
</td>
<td className="text-center py-1 px-3 border border-gray-300 bg-white">
{formatNumber(row.rainwater)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
};
export default ReuseStatus;
@@ -0,0 +1,70 @@
"use client";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { getCoreRowModel } from "@tanstack/react-table";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import {
scheduleTableColumns,
} from "../model/table-columns";
import { getScheduleList } from "../api/schedule-service";
// 날짜를 YYYY-MM-DD 형식으로 포맷팅
const formatDate = (date: Date): string => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};
// 오늘부터 한 달 후까지의 날짜 계산
const getDateRange = () => {
const today = new Date();
const oneMonthLater = new Date();
oneMonthLater.setMonth(today.getMonth() + 1);
return {
startDate: formatDate(today),
endDate: formatDate(oneMonthLater),
};
};
const ScheduleList: React.FC = () => {
const router = useRouter();
const { startDate, endDate } = getDateRange();
const scheduleQuery = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, startDate, endDate],
queryFn: () =>
getScheduleList({
siteId: "",
schdlTtl: "",
schdlSeCd: "",
schdlBgngYmd: startDate,
schdlEndYmd: endDate,
reptYn: "",
commSchdlYn: "Y",
}),
});
const { tableComponent: scheduleTableComponent } = useDataTable({
query: scheduleQuery,
tableOptions: {
columns: scheduleTableColumns,
getCoreRowModel: getCoreRowModel(),
},
dataTableOptions: {
variant: "line",
},
});
return (
<div className="card card-border !p-0" style={{ height: "300px" }}>
{scheduleTableComponent}
</div>
);
};
export default ScheduleList;
@@ -0,0 +1,184 @@
"use client";
/**
* 노후 하수관로 현황
*/
import React, { useEffect, useState } from "react";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
type ChartOptions,
} from "chart.js";
import { Bar } from "react-chartjs-2";
import { Button } from "@common/components";
import type { PplLengthContent, ChartDataSet, OldPplChartData } from "../model/sn-interface";
import { SewerageApi, useOldPplStore,useMapOldPplStore, transformChartDataToMapData } from "../api/sn-service";
// ChartJS 컴포넌트 등록
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
export default function SnOldPpl() {
const [oldPplLenData, setOldPplLenData] = useState<PplLengthContent[]>([]);
const {chartData, setChartData} = useOldPplStore();
const {setMapData} = useMapOldPplStore();
useEffect(() => {
// 유역별, 노후년도별 관로길이 데이터를 한번에 조회
SewerageApi.getMvOldPplLength()
.then((apiResult) => {
setOldPplLenData(apiResult);
})
.catch((e) => console.error("노후관로 데이터 조회 실패", e));
}, []);
useEffect(() => {
if (oldPplLenData.length === 0) return;
// 유역 목록 추출 (중복 제거)
const allZones = [...new Set(oldPplLenData.map(d => d.zoneNm))];
// yearGroup 매핑: API 응답값 -> 차트 label
const yearGroupMap: { [key: string]: { label: string; color: string } } = {
"20년~25년미만": { label: "20-25년", color: "#3B82F6" },
"25년~30년미만": { label: "25-30년", color: "#EF4444" },
"30년이상": { label: "30년이상", color: "#F87171" },
};
// 유역별, 노후년도별 데이터 구조화
const dataByZone: { [zone: string]: { [yearGroup: string]: number } } = {};
allZones.forEach(zone => {
dataByZone[zone] = {};
});
oldPplLenData.forEach(item => {
let zoneData = dataByZone[item.zoneNm];
if (!zoneData) {
zoneData = {};
dataByZone[item.zoneNm] = zoneData;
}
if (item.yearGroup) {
zoneData[item.yearGroup] = item.sumPrlgVl;
}
});
// 데이터셋 생성
const newDatasets: ChartDataSet[] = Object.entries(yearGroupMap).map(([yearGroup, { label, color }]) => ({
label,
data: allZones.map(zone => dataByZone[zone]?.[yearGroup] ?? 0),
backgroundColor: color,
borderRadius: 4,
}));
const newChartData: OldPplChartData = {
labels: allZones,
datasets: newDatasets,
};
setChartData(newChartData);
// 차트 데이터를 기반으로 지도 팝업 데이터 생성 및 상태 업데이트
setMapData(transformChartDataToMapData(newChartData));
}, [oldPplLenData]);
// 각 지역별 총 길이 계산
const totalLengths = chartData.labels.map((label, i) => {
return chartData.datasets.reduce((sum, dataset) => {
const value = dataset.data[i];
return sum + (typeof value === 'number' ? value : 0);
}, 0);
});
// 차트 옵션 설정
const options: ChartOptions<"bar"> = {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
grid: {
display: true,
},
},
y: {
beginAtZero: true,
min: 0,
max: 10000,
ticks: {
stepSize: 2000,
callback: function (value) {
return Number(value).toLocaleString() + 'km';
}
},
grid: {
color: 'rgba(200, 200, 200, 0.3)',
}
}
},
plugins: {
legend: {
position: 'top',
align: 'center', // ⬅ 가운데 정렬
labels: {
boxWidth: 15,
padding: 20, // ⬅ 범례 항목 간 여백 확대
usePointStyle: false,
},
},
tooltip: {
callbacks: {
label: function (context) {
return `${context.dataset.label}: ${Number(context.parsed.y).toLocaleString()}km`;
}
}
},
},
// 총 길이 데이터 표시를 위한 애니메이션 후 함수 설정
animation: {
onComplete: function (animation) {
const chart = animation.chart;
const ctx = chart.ctx;
// 각 데이터셋의 마지막 데이터셋의 인덱스
if (chartData.datasets.length === 0) return;
// const lastMeta = chart.getDatasetMeta(chartData.datasets.length - 1);
// 텍스트 스타일 설정
ctx.font = '11px Arial';
ctx.fillStyle = '#333';
ctx.textAlign = 'center';
// 각 지역별로 총 길이 표시
chartData.labels.forEach((_, i) => {
const xPos = chart.scales.x ? chart.scales.x.getPixelForTick(i) : 0;
const yPos = chart.chartArea.top; // 차트 영역 최상단 위에 표시
// 막대 위에 총 길이 텍스트 표시
if (totalLengths[i]) ctx.fillText(`총 ${totalLengths[i]?.toLocaleString()}km`, xPos, yPos);
});
}
}
};
const handleMoreClick = () => {
window.open(process.env.NEXT_PUBLIC_SN, "_blank", "noopener,noreferrer");
};
return (
<div className="card card-border" style={{ height: "250px" }}>
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">노후 하수관로 현황</h3>
<Button className="h-[3.2rem]" onClick={handleMoreClick}>더보기</Button>
</div>
<div className="h-70 p-0 flex items-center justify-center">
<div style={{ width: "100%", maxWidth: 400, height: 180 }}>
<Bar options={options} data={chartData} />
</div>
</div>
</div>
);
}
@@ -0,0 +1,172 @@
"use client";
/**
* 오수관로 및 우수관로 현황
*/
import React, { useEffect, useState } from "react";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
type ChartOptions,
} from "chart.js";
import { Bar } from "react-chartjs-2";
import { Button } from "@common/components";
import type { PplLengthContent } from "../model/sn-interface";
import { SewerageApi,useUsePplStore } from "../api/sn-service";
interface ChartDataSet {
label: string;
data: (number | undefined)[];
backgroundColor: string;
borderColor?: string;
}
// const usgCd = { "SBA004": "오수관로", "SBA003": "우수관로", "ETC": "기타" }; // 용도
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
export default function SnPplStatus() {
const {chartData, setChartData} = useUsePplStore();
const [usgPplLenData, setUsgPplLenData] = useState<PplLengthContent[]>([]);
useEffect(() => {
// 유역별, 용도별 관로길이 데이터를 한번에 조회
SewerageApi.getMvUsgPplLength()
.then((apiResult) => {
setUsgPplLenData(apiResult);
})
.catch((e) => console.error("용도별 관로 데이터 조회 실패", e));
}, []);
// 데이터가 변경될 때마다 차트를 다시 계산
useEffect(() => {
if (usgPplLenData.length === 0) return;
// 유역 목록 추출 (중복 제거)
const allZones = [...new Set(usgPplLenData.map(d => d.zoneNm))];
const labels = allZones;
// 유역별, 용도별 데이터 구조화
const dataByZone: { [key: string]: { sewage: number; stormwater: number; etc: number } } = {};
labels.forEach(zone => {
dataByZone[zone] = { sewage: 0, stormwater: 0, etc: 0 };
});
// usgCd별로 데이터 분류: SBA004(오수관로), SBA003(우수관로), ETC(기타)
usgPplLenData.forEach(item => {
if (!dataByZone[item.zoneNm]) {
dataByZone[item.zoneNm] = { sewage: 0, stormwater: 0, etc: 0 };
}
if (item.usgCd === "SBA004") {
dataByZone[item.zoneNm]!.sewage = item.sumPrlgVl;
} else if (item.usgCd === "SBA003") {
dataByZone[item.zoneNm]!.stormwater = item.sumPrlgVl;
} else if (item.usgCd === "ETC") {
dataByZone[item.zoneNm]!.etc = item.sumPrlgVl;
}
});
const datasets: ChartDataSet[] = [];
// 기타 데이터셋 (맨 아래에 쌓기 위해 먼저 추가)
datasets.push({
label: "기타",
data: labels.map(zone => dataByZone[zone]?.etc),
backgroundColor: "rgba(255, 178, 150, 0.9)",
});
// 오수관로 데이터셋
datasets.push({
label: "오수관로",
data: labels.map(zone => dataByZone[zone]?.sewage),
backgroundColor: "rgba(180, 240, 190, 0.95)",
});
// 우수관로 데이터셋
datasets.push({
label: "우수관로",
data: labels.map(zone => dataByZone[zone]?.stormwater),
backgroundColor: "rgba(170, 210, 245, 0.95)",
});
setChartData({ labels, datasets });
}, [usgPplLenData]);
// 총합(라벨당) 계산
const totals = chartData.labels.map((_, i) =>
chartData.datasets.reduce((s, ds) => s + (ds.data[i] as number), 0)
);
const options: ChartOptions<"bar"> = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "top",
},
tooltip: {
callbacks: {
label: (ctx) => `${ctx.dataset.label}: ${Number(ctx.parsed.y).toLocaleString()}km`,
},
},
},
scales: {
x: {
stacked: true,
},
y: {
stacked: true,
beginAtZero: true,
ticks: {
callback: (value) => `${Number(value).toLocaleString()}km`,
},
},
},
// 각 스택 위에 총합 표시
animation: {
onComplete: function (animation) {
const chart = animation.chart;
const ctx = chart.ctx;
const lastMeta = chart.getDatasetMeta(chartData.datasets.length - 1);
ctx.save();
ctx.fillStyle = "#111";
ctx.font = "gray bold 12px Arial";
ctx.textAlign = "center";
totals.forEach((total, i) => {
const bar = lastMeta.data[i];
if (!bar) return;
const x = bar.x;
// y는 마지막 스택의 y 좌표(스택의 최상단)에서 조금 위로
const y = bar.y - 8;
ctx.fillText(`총 ${total.toLocaleString()}km`, x, y);
});
ctx.restore();
},
},
};
const handleMoreClick = () => {
window.open(process.env.NEXT_PUBLIC_SN, "_blank", "noopener,noreferrer");
};
return (
<div className="card card-border" style={{ height: "250px" }}>
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">오수관로 및 우수관로 현황</h3>
<Button className="h-[3.2rem]" onClick={handleMoreClick}>더보기</Button>
</div>
<div className="h-60 p-4">
<div style={{ width: "100%", maxWidth: 400, height: 180 }}>
<Bar options={options} data={chartData} />
</div>
</div>
</div>
);
}
@@ -0,0 +1,40 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { getCoreRowModel } from "@tanstack/react-table";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import type { PageableList } from "@/shared/model";
import { supportTableColumns } from "../model/support-columns";
import { getSupportList, type Support } from "../api/support-service";
const SupportList: React.FC = () => {
const supportQuery = useQuery<PageableList<Support>>({
queryKey: [ENDPOINT.IC_SERVICE.GROUPING_STATISTICS],
queryFn: async () => {
return await getSupportList();
},
});
const { tableComponent: supportTableComponent } = useDataTable({
query: supportQuery,
tableOptions: {
columns: supportTableColumns,
getCoreRowModel: getCoreRowModel(),
},
dataTableOptions: {
variant: "line",
},
});
return (
<div className="card card-border !p-0" style={{ height: "220px" }}>
<div className="card-header !m-0 !border-none text-foreground">
<h3 className="text-2xl font-medium">지원현황</h3>
</div>
{supportTableComponent}
</div>
);
};
export default SupportList;
+19 -4
View File
@@ -44,7 +44,7 @@ export const ENDPOINT = {
ORGANIZATIONS: "/cms-service/api/v1/ognzs",
USER_ORGANIZATION: "/cms-service/api/v1/user-ognz",
IMAGES: "/cms-service/api/v1/images",
BANNERS: "/cms-service/api/v1/bnrs",
BANNERS: "/cms-service/api/v1/bnrs/user",
TEMPLATE: "/cms-service/api/v1/tmplt",
TERMS: "/cms-service/api/v1/trms",
LWPO_CODE: "/cms-service/api/v1/lwpo-cds",
@@ -61,23 +61,38 @@ export const ENDPOINT = {
APILOG: "/cms-service/api/v1/api-logs",
SITE_AUTHORITY: "/cms-service/api/v1/site-authrt",
CONNECTION_LOG: "/cms-service/api/v1/cntnlog",
SCEHDULES: "/cms-service/api/v1/schdls",
},
BOARD_SERVICE: {
BBS: "/board-service/api/v1/bbs",
BBS_TEMPLATES: "/board-service/api/v1/bbsTmplt",
POSTS: "/board-service/api/v1/posts",
MY_POSTS: "/board-service/api/v1/posts/me",
POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts",
POST_EVALUATION: "/board-service/api/v1/pstEvl",
SITE_AUTHORITY_LIST: "/cms-service/api/v1/site-authrt/user",
CONNECTION_LOG_LIST: "/cms-service/api/v1/cntnlog/user",
},
SEWERAGE_SERVICE: {
PPL_LENGTH: "/sewerage-service/api/v1/ms/pplLength",
PPL_USE_LENGTH: "/sewerage-service/api/v1/ms/pplUseLength",
PPL_COUNTS: "/sewerage-service/api/v1/ms/pplCounts",
OLD_PPL_COUNTS: "/sewerage-service/api/v1/ms/oldPplCounts",
},
MV_USG_PPL_LENGTH: "/sewerage-service/api/v1/ms/mvUsgPplLength",
MV_OLD_PPL_LENGTH: "/sewerage-service/api/v1/ms/mvOldPplLength",
},
OPERATION_SERVICE: {
MAIN_YEAR: "/operation-service/api/v1/dashBoard/main/year",
OPER_CNTRT: "/operation-service/api/v1/dashBoard/main/oper/cntrt",
OPER_WQLT: "/operation-service/api/v1/dashBoard/main/oper/wqlt",
REUSE_STATUS: "/operation-service/api/v1/dashBoard/main/reuse/status",
REUSE_MAP: "/operation-service/api/v1/dashBoard/main/reuse/map",
},
IC_SERVICE: {
GROUPING_STATISTICS: "/ic-service/api/v1/nti/grouping-statistics",
GROUPING_STATISTICS: "/ic-service/api/v1/support-status-summary",
},
DC_SERVICE: {
DATA_STATUS: "/dc-service/api/v1/dc/dataStatus",
DATA_STATUS: "/dc-service/api/v1/dataStatus",
},
};
+15 -14
View File
@@ -1,3 +1,4 @@
import React from "react";
import { type UseQueryResult } from "@tanstack/react-query";
import type { PageableList } from "../model";
@@ -9,24 +10,24 @@ import type { PageableList } from "../model";
export function matchQueryStatus<T>(
query: UseQueryResult<PageableList<T>>,
options: {
Loading?: JSX.Element;
Errored?: JSX.Element | ((error: unknown) => JSX.Element);
Empty?: JSX.Element;
Loading?: React.ReactElement;
Errored?: React.ReactElement | ((error: unknown) => React.ReactElement);
Empty?: React.ReactElement;
Success: (
data: UseQueryResult<PageableList<T>> & {
data: NonNullable<UseQueryResult<T>["data"]>;
},
) => JSX.Element;
) => React.ReactElement;
},
): JSX.Element;
): React.ReactElement;
export function matchQueryStatus<T>(
query: UseQueryResult<PageableList<T>>,
options: {
Loading?: JSX.Element;
Errored?: JSX.Element | ((error: unknown) => JSX.Element);
Success: (data: UseQueryResult<PageableList<T>>) => JSX.Element;
Loading?: React.ReactElement;
Errored?: React.ReactElement | ((error: unknown) => React.ReactElement);
Success: (data: UseQueryResult<PageableList<T>>) => React.ReactElement;
},
): JSX.Element;
): React.ReactElement;
export function matchQueryStatus<T>(
query: UseQueryResult<PageableList<T>>,
{
@@ -35,12 +36,12 @@ export function matchQueryStatus<T>(
Empty,
Success,
}: {
Loading?: JSX.Element;
Errored?: JSX.Element | ((error: unknown) => JSX.Element);
Empty?: JSX.Element;
Success: (data: UseQueryResult<PageableList<T>>) => JSX.Element;
Loading?: React.ReactElement;
Errored?: React.ReactElement | ((error: unknown) => React.ReactElement);
Empty?: React.ReactElement;
Success: (data: UseQueryResult<PageableList<T>>) => React.ReactElement;
},
): JSX.Element {
): React.ReactElement {
if (query.isLoading && Loading) {
return Loading;
}
@@ -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.currentMenuInfo);
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 };
+1 -18
View File
@@ -1,22 +1,5 @@
import { PageTitle } from "@common/components";
import { useMenuStore } from "../lib/use-menu-store";
function PageLayout({ children }: { children: React.ReactNode }) {
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
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>
);
return <div className="h-auto min-h-full bg-container pb-2">{children}</div>;
}
export { PageLayout };
+2 -2
View File
@@ -23,12 +23,12 @@
"@tanstack/react-query": "^5.67.1",
"@tanstack/react-query-devtools": "^5.75.5",
"@tanstack/react-table": "^8.21.2",
"@tinymce/tinymce-react": "^6.1.0",
"@tinymce/tinymce-react": "^5.1.1",
"axios": "^1.8.1",
"chart.js": "^4.4.8",
"dayjs": "^1.11.13",
"js-cookie": "^3.0.5",
"next": "15.3.0",
"next": "15.3.8",
"next-themes": "^0.4.6",
"react-chartjs-2": "^5.3.0",
"react-dnd": "^16.0.1",
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

File diff suppressed because one or more lines are too long
@@ -0,0 +1,91 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
/**
* Jquery integration plugin.
*
* @class tinymce.core.JqueryIntegration
* @private
*/
!function(){function f(){
// Reference to tinymce needs to be lazily evaluated since tinymce
// might be loaded through the compressor or other means
return d.tinymce}var p,c,u,s=[],d="undefined"!=typeof global?global:window,m=d.jQuery;m.fn.tinymce=function(o){var e,t,i,n,l=this,r="";
// No match then just ignore the call
return l.length?
// Get editor instance
o?(l.css("visibility","hidden"),
// Load TinyMCE on demand, if we need to
d.tinymce||c||!(e=o.script_url)?
// Delay the init call until tinymce is loaded
1===c?s.push(a):a():(c=1,t=e.substring(0,e.lastIndexOf("/")),
// Check if it's a dev/src version they want to load then
// make sure that all plugins, themes etc are loaded in source mode as well
-1!=e.indexOf(".min")&&(r=".min"),
// Setup tinyMCEPreInit object this will later be used by the TinyMCE
// core script to locate other resources like CSS files, dialogs etc
// You can also predefined a tinyMCEPreInit object and then it will use that instead
d.tinymce=d.tinyMCEPreInit||{base:t,suffix:r},
// url contains gzip then we assume it's a compressor
-1!=e.indexOf("gzip")&&(i=o.language||"en",e=e+(/\?/.test(e)?"&":"?")+"js=true&core=true&suffix="+escape(r)+"&themes="+escape(o.theme||"modern")+"&plugins="+escape(o.plugins||"")+"&languages="+(i||""),
// Check if compressor script is already loaded otherwise setup a basic one
d.tinyMCE_GZ||(d.tinyMCE_GZ={start:function(){function n(e){f().ScriptLoader.markDone(f().baseURI.toAbsolute(e))}
// Add core languages
n("langs/"+i+".js"),
// Add themes with languages
n("themes/"+o.theme+"/theme"+r+".js"),n("themes/"+o.theme+"/langs/"+i+".js"),
// Add plugins with languages
m.each(o.plugins.split(","),function(e,t){t&&(n("plugins/"+t+"/plugin"+r+".js"),n("plugins/"+t+"/langs/"+i+".js"))})},end:function(){}})),(n=document.createElement("script")).type="text/javascript",n.onload=n.onreadystatechange=function(e){e=e||window.event,2===c||"load"!=e.type&&!/complete|loaded/.test(n.readyState)||(f().dom.Event.domLoaded=1,c=2,
// Execute callback after mainscript has been loaded and before the initialization occurs
o.script_loaded&&o.script_loaded(),a(),m.each(s,function(e,t){t()}))},n.src=e,document.body.appendChild(n)),l):f()?f().get(l[0].id):null:l;function a(){var a=[],c=0;
// Apply patches to the jQuery object, only once
u||(v(),u=!0),
// Create an editor instance for each matched node
l.each(function(e,t){var n,i=t.id,r=o.oninit;
// Generate unique id for target element if needed
i||(t.id=i=f().DOM.uniqueId()),
// Only init the editor once
f().get(i)||(
// Create editor instance and render it
n=f().createEditor(i,o),a.push(n),n.on("init",function(){var e,t=r;l.css("visibility",""),
// Run this if the oninit setting is defined
// this logic will fire the oninit callback ones each
// matched editor instance is initialized
r&&++c==a.length&&("string"==typeof t&&(e=-1===t.indexOf(".")?null:f().resolve(t.replace(/\.\w+$/,"")),t=f().resolve(t)),
// Call the oninit function with the object
t.apply(e||f(),a))}))}),
// Render the editor instances in a separate loop since we
// need to have the full editors array used in the onInit calls
m.each(a,function(e,t){t.render()})}},
// Add :tinymce pseudo selector this will select elements that has been converted into editor instances
// it's now possible to use things like $('*:tinymce') to get all TinyMCE bound elements.
m.extend(m.expr[":"],{tinymce:function(e){var t;return!!(e.id&&"tinymce"in d&&(t=f().get(e.id))&&t.editorManager===f())}});
// This function patches internal jQuery functions so that if
// you for example remove an div element containing an editor it's
// automatically destroyed by the TinyMCE API
var v=function(){function r(e){
// If the function is remove
"remove"===e&&this.each(function(e,t){var n=u(t);n&&n.remove()}),this.find("span.mceEditor,div.mceEditor").each(function(e,t){var n=f().get(t.id.replace(/_parent$/,""));n&&n.remove()})}function o(i){var e,t=this;
// Handle set value
/*jshint eqnull:true */if(null!=i)r.call(t),
// Saves the contents before get/set value of textarea/div
t.each(function(e,t){var n;(n=f().get(t.id))&&n.setContent(i)});else if(0<t.length&&(e=f().get(t[0].id)))return e.getContent()}function l(e){return e&&e.length&&d.tinymce&&e.is(":tinymce")}
// Removes any child editor instances by looking for editor wrapper elements
var u=function(e){var t=null;return e&&e.id&&d.tinymce?f().get(e.id):t},s={};
// Loads or saves contents from/to textarea if the value
// argument is defined it will set the TinyMCE internal contents
// Patch some setter/getter functions these will
// now be able to set/get the contents of editor instances for
// example $('#editorid').html('Content'); will update the TinyMCE iframe instance
m.each(["text","html","val"],function(e,t){var a=s[t]=m.fn[t],c="text"===t;m.fn[t]=function(e){var t=this;if(!l(t))return a.apply(t,arguments);if(e!==p)return o.call(t.filter(":tinymce"),e),a.apply(t.not(":tinymce"),arguments),t;// return original set for chaining
var i="",r=arguments;return(c?t:t.eq(0)).each(function(e,t){var n=u(t);i+=n?c?n.getContent().replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g,""):n.getContent({save:!0}):a.apply(m(t),r)}),i}}),
// Makes it possible to use $('#id').append("content"); to append contents to the TinyMCE editor iframe
m.each(["append","prepend"],function(e,t){var n=s[t]=m.fn[t],r="prepend"===t;m.fn[t]=function(i){var e=this;return l(e)?i!==p?("string"==typeof i&&e.filter(":tinymce").each(function(e,t){var n=u(t);n&&n.setContent(r?i+n.getContent():n.getContent()+i)}),n.apply(e.not(":tinymce"),arguments),e):void 0:n.apply(e,arguments)}}),
// Makes sure that the editor instance gets properly destroyed when the parent element is removed
m.each(["remove","replaceWith","replaceAll","empty"],function(e,t){var n=s[t]=m.fn[t];m.fn[t]=function(){return r.call(this,t),n.apply(this,arguments)}}),s.attr=m.fn.attr,
// Makes sure that $('#tinymce_id').attr('value') gets the editors current HTML contents
m.fn.attr=function(e,t){var n=this,i=arguments;if(!e||"value"!==e||!l(n))return s.attr.apply(n,i);if(t!==p)return o.call(n.filter(":tinymce"),t),s.attr.apply(n.not(":tinymce"),i),n;// return original set for chaining
var r=n[0],a=u(r);return a?a.getContent({save:!0}):s.attr.apply(m(r),i)}}}();
@@ -0,0 +1,3 @@
This is where language files should be placed.
Please DO NOT translate these directly use this service: https://www.transifex.com/projects/p/tinymce/
File diff suppressed because one or more lines are too long
@@ -0,0 +1,504 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";function f(t,e,n){t.execCommand("UL"===e?"InsertUnorderedList":"InsertOrderedList",!1,!1===n?null:{"list-style-type":n})}function i(t){return function(){return t}}function t(t){return t}function e(){return s}var n=tinymce.util.Tools.resolve("tinymce.PluginManager"),g=tinymce.util.Tools.resolve("tinymce.util.Tools"),u=i(!1),l=i(!0),s={fold:function(t,e){return t()},isSome:u,isNone:l,getOr:t,getOrThunk:r,getOrDie:function(t){throw new Error(t||"error: getOrDie called on none.")},getOrNull:i(null),getOrUndefined:i(void 0),or:t,orThunk:r,map:e,each:function(){},bind:e,exists:u,forall:l,filter:function(){return s},toArray:function(){return[]},toString:i("none()")};function r(t){return t()}function d(t){return t&&/^(TH|TD)$/.test(t.nodeName)}function m(c,a){return function(s){function t(t){var e,n,r,o,i,u,l;s.setActive((e=c,r=a,i=-1!==(o=function(t,e){for(var n=0;n<t.length;n++)if(e(t[n]))return n;return-1}((n=t).parents,d))?n.parents.slice(0,o):n.parents,0<(u=g.grep(i,(l=e,function(t){return t&&/^(OL|UL|DL)$/.test(t.nodeName)&&(e=t,l.$.contains(l.getBody(),e));var e}))).length&&u[0].nodeName===r))}return c.on("NodeChange",t),function(){return c.off("NodeChange",t)}}}function c(t,e,n,r,o,i){var u,l,s,c,a,d;1<i.length?(c=r,a=o,d=i,(s=t).ui.registry.addSplitButton(e,{tooltip:n,icon:"OL"===a?"ordered-list":"unordered-list",presets:"listpreview",columns:3,fetch:function(t){t(g.map(d,function(t){return{type:"choiceitem",value:"default"===t?"":t,icon:"list-"+("OL"===a?"num":"bull")+"-"+("disc"===t||"decimal"===t?"default":t),text:t.replace(/\-/g," ").replace(/\b\w/g,function(t){return t.toUpperCase()})}}))},onAction:function(){return s.execCommand(c)},onItemAction:function(t,e){f(s,a,e)},select:function(e){var t,n=(t=s).dom.getParent(t.selection.getNode(),"ol,ul"),r=t.dom.getStyle(n,"listStyleType");return p(r).map(function(t){return e===t}).getOr(!1)},onSetup:m(s,a)})):(l=r,(u=t).ui.registry.addToggleButton(e,{active:!1,tooltip:n,icon:"OL"===o?"ordered-list":"unordered-list",onSetup:m(u,o),onAction:function(){return u.execCommand(l)}}))}var a=function(n){function t(){return o}function e(t){return t(n)}var r=i(n),o={fold:function(t,e){return e(n)},isSome:l,isNone:u,getOr:r,getOrThunk:r,getOrDie:r,getOrNull:r,getOrUndefined:r,or:t,orThunk:t,map:function(t){return a(t(n))},each:function(t){t(n)},bind:e,exists:e,forall:e,filter:function(t){return t(n)?o:s},toArray:function(){return[n]},toString:function(){return"some("+n+")"}};return o},p=function(t){return null==t?s:a(t)};n.add("advlist",function(t){var n,e,r,o;t.hasPlugin("lists")?(c(e=t,"numlist","Numbered list","InsertOrderedList","OL",(r=e.getParam("advlist_number_styles","default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman"))?r.split(/[ ,]/):[]),c(e,"bullist","Bullet list","InsertUnorderedList","UL",(o=e.getParam("advlist_bullet_styles","default,circle,square"))?o.split(/[ ,]/):[]),(n=t).addCommand("ApplyUnorderedListStyle",function(t,e){f(n,"UL",e["list-style-type"])}),n.addCommand("ApplyOrderedListStyle",function(t,e){f(n,"OL",e["list-style-type"])})):console.error("Please use the Lists plugin together with the Advanced List plugin.")})}();
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";function a(e){return e.getAttribute("id")||e.getAttribute("name")||""}function c(e){return e&&"a"===e.nodeName.toLowerCase()&&!e.getAttribute("href")&&""!==a(e)}function d(e){return e.dom.getParent(e.selection.getStart(),l)}function r(e,t){var o,a,n,r,i,l=d(e);l?(n=e,r=t,(i=l).removeAttribute("name"),i.id=r,n.addVisual(),n.undoManager.add()):(a=t,(o=e).undoManager.transact(function(){var e,n;o.getParam("allow_html_in_named_anchor",!1,"boolean")||o.selection.collapse(!0),o.selection.isCollapsed()?o.insertContent(o.dom.createHTML("a",{id:a})):(n=(e=o).dom,u(n).walk(e.selection.getRng(),function(e){s.each(e,function(e){var t;c(t=e)&&!t.firstChild&&n.remove(e,!1)})}),o.formatter.remove("namedAnchor",null,null,!0),o.formatter.apply("namedAnchor",{value:a}),o.addVisual())})),e.focus()}function i(r){return function(e){for(var t,n=0;n<e.length;n++){var o=e[n],a=void 0;!(a=t=o)||a.attr("href")||!a.attr("id")&&!a.attr("name")||t.firstChild||o.attr("contenteditable",r)}}}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),u=tinymce.util.Tools.resolve("tinymce.dom.RangeUtils"),s=tinymce.util.Tools.resolve("tinymce.util.Tools"),l="a:not([href])";e.add("anchor",function(e){var t,n,o;(t=e).on("PreInit",function(){t.parser.addNodeFilter("a",i("false")),t.serializer.addNodeFilter("a",i(null))}),(n=e).addCommand("mceAnchor",function(){var o,e,t;t=(e=d(o=n))?a(e):"",o.windowManager.open({title:"Anchor",size:"normal",body:{type:"panel",items:[{name:"id",type:"input",label:"ID",placeholder:"example"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{id:t},onSubmit:function(e){var t=o,n=e.getData().id;(/^[A-Za-z][A-Za-z0-9\-:._]*$/.test(n)?(r(t,n),0):(t.windowManager.alert("Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores."),1))||e.close()}})}),(o=e).ui.registry.addToggleButton("anchor",{icon:"bookmark",tooltip:"Anchor",onAction:function(){return o.execCommand("mceAnchor")},onSetup:function(e){return o.selection.selectorChangedWithUnbind("a:not([href])",e.setActive).unbind}}),o.ui.registry.addMenuItem("anchor",{icon:"bookmark",text:"Anchor...",onAction:function(){return o.execCommand("mceAnchor")}}),e.on("PreInit",function(){e.formatter.register("namedAnchor",{inline:"a",selector:l,remove:"all",split:!0,deep:!0,attributes:{id:"%value"},onmatch:c})})})}();
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";function k(e){return/^[(\[{ \u00a0]$/.test(e)}function w(e){return 3===e.nodeType}function i(e){return 1===e.nodeType}function o(e,t){var n;return t<0&&(t=0),!w(e)||(n=e.data.length)<t&&(t=n),t}function y(e,t,n){!i(t)||t.hasChildNodes()?e.setStart(t,o(t,n)):e.setStartBefore(t)}function v(e,t,n){!i(t)||t.hasChildNodes()?e.setEnd(t,o(t,n)):e.setEndAfter(t)}function r(e,t){var n,i,o,r,a,f=e.getParam("autolink_pattern",A),s=e.getParam("default_link_target",!1);if(null===e.dom.getParent(e.selection.getNode(),"a[href]")){var d=e.selection.getRng().cloneRange();if(d.startOffset<5){if(!(r=d.endContainer.previousSibling)){if(!d.endContainer.firstChild||!d.endContainer.firstChild.nextSibling)return;r=d.endContainer.firstChild.nextSibling}if(y(d,r,a=r.length),v(d,r,a),d.endOffset<5)return;n=d.endOffset,i=r}else{if(!w(i=d.endContainer)&&i.firstChild){for(;!w(i)&&i.firstChild;)i=i.firstChild;w(i)&&(y(d,i,0),v(d,i,i.nodeValue.length))}n=1===d.endOffset?2:d.endOffset-1-t}for(var l=n;y(d,i,2<=n?n-2:0),v(d,i,1<=n?n-1:0),--n,!k(d.toString())&&0<=n-2;);k(d.toString())?(y(d,i,n),v(d,i,l),n+=1):(0===d.startOffset?y(d,i,0):y(d,i,n),v(d,i,l)),u=d.toString(),/[?!,.;:]/.test(u.charAt(u.length-1))&&v(d,i,l-1);var u,c,g,h,C=(u=d.toString().trim()).match(f),m=e.getParam("link_default_protocol","http","string");C&&((g=c=C[0]).length>=(h="www.").length&&g.substr(0,0+h.length)===h?c=m+"://"+c:-1===c.indexOf("@")||/^([A-Za-z][A-Za-z\d.+-]*:\/\/)|mailto:/.test(c)||(c="mailto:"+c),o=e.selection.getBookmark(),e.selection.setRng(d),e.execCommand("createlink",!1,c),!1!==s&&e.dom.setAttrib(e.selection.getNode(),"target",s),e.selection.moveToBookmark(o),e.nodeChanged())}}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=tinymce.util.Tools.resolve("tinymce.Env"),A=new RegExp("^"+/(?:[A-Za-z][A-Za-z\d.+-]{0,14}:\/\/(?:[-.~*+=!&;:'%@?^${}(),\w]+@)?|www\.|[-;:&=+$,.\w]+@)[A-Za-z\d-]+(?:\.[A-Za-z\d-]+)*(?::\d+)?(?:\/(?:[-.~*+=!;:'%@$(),\/\w]*[-~*+=%@$()\/\w])?)?(?:\?(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?(?:#(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?/g.source+"$","i");e.add("autolink",function(e){var t,n;(t=e).on("keydown",function(e){if(13===e.keyCode)return r(t,-1)}),a.browser.isIE()?t.on("focus",function(){if(!n){n=!0;try{t.execCommand("AutoUrlDetect",!1,!0)}catch(e){}}}):(t.on("keypress",function(e){if(41===e.keyCode||93===e.keyCode||125===e.keyCode)return r(t,-1)}),t.on("keyup",function(e){if(32===e.keyCode)return r(t,0)}))})}();
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";function y(e){return e.getParam("min_height",e.getElement().offsetHeight,"number")}function p(e,t){var n=e.getBody();n&&(n.style.overflowY=t?"":"hidden",t||(n.scrollTop=0))}function v(e,t,n,i){var o=parseInt(e.getStyle(t,n,i),10);return isNaN(o)?0:o}var l=Object.hasOwnProperty,e=tinymce.util.Tools.resolve("tinymce.PluginManager"),b=tinymce.util.Tools.resolve("tinymce.Env"),r=tinymce.util.Tools.resolve("tinymce.util.Delay"),u=function(e,t,n,i,o){r.setEditorTimeout(e,function(){C(e,t),n--?u(e,t,n,i,o):o&&o()},i)},C=function(e,t,n){var i,o,r,s,a,l,u,g,c,m,f,d=e.dom,h=e.getDoc();h&&(e.plugins.fullscreen&&e.plugins.fullscreen.isFullscreen()?p(e,!0):(i=h.documentElement,o=e.getParam("autoresize_bottom_margin",50,"number"),r=y(e),s=v(d,i,"margin-top",!0),a=v(d,i,"margin-bottom",!0),(l=(l=i.offsetHeight+s+a+o)<0?0:l)+(u=e.getContainer().offsetHeight-e.getContentAreaContainer().offsetHeight)>y(e)&&(r=l+u),(g=e.getParam("max_height",0,"number"))&&g<r?(r=g,p(e,!0)):p(e,!1),r!==t.get()&&(c=r-t.get(),d.setStyle(e.getContainer(),"height",r+"px"),t.set(r),e.fire("ResizeEditor"),b.browser.isSafari()&&b.mac&&(m=e.getWin()).scrollTo(m.pageXOffset,m.pageYOffset),!e.hasFocus()||"setcontent"!==(null==(f=n)?void 0:f.type.toLowerCase())||!0!==f.selection&&!0!==f.paste||e.selection.scrollIntoView(),b.webkit&&c<0&&C(e,t,n))))};e.add("autoresize",function(e){var t,n,i,o,r,s,a=e.settings;l.call(a,"resize")||(e.settings.resize=!1),e.inline||(s=0,r=t={get:function(){return s},set:function(e){s=e}},(o=e).addCommand("mceAutoResize",function(){C(o,r)}),i=t,(n=e).on("init",function(){var e=n.getParam("autoresize_overflow_padding",1,"number"),t=n.dom;t.setStyles(n.getDoc().documentElement,{height:"auto"}),t.setStyles(n.getBody(),{paddingLeft:e,paddingRight:e,"min-height":0})}),n.on("NodeChange SetContent keyup FullscreenStateChanged ResizeContent",function(e){C(n,i,e)}),n.getParam("autoresize_on_init",!0,"boolean")&&n.on("init",function(){u(n,i,20,100,function(){u(n,i,5,1e3)})}))})}();
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";function o(t,e){var r=t||e,n=/^(\d+)([ms]?)$/.exec(""+r);return(n[2]?{s:1e3,m:6e4}[n[2]]:1)*parseInt(r,10)}function n(t){var e=document.location;return t.getParam("autosave_prefix","tinymce-autosave-{path}{query}{hash}-{id}-").replace(/{path}/g,e.pathname).replace(/{query}/g,e.search).replace(/{hash}/g,e.hash).replace(/{id}/g,t.id)}function i(t,e){if(a(e))return t.dom.isEmpty(t.getBody());var r=d.trim(e);if(""===r)return!0;var n=(new DOMParser).parseFromString(r,"text/html");return t.dom.isEmpty(n)}function u(t){var e=parseInt(v.getItem(n(t)+"time"),10)||0;return!((new Date).getTime()-e>o(t.getParam("autosave_retention"),"20m")&&(g(t,!1),1))}function s(t){var e=n(t);!i(t)&&t.isDirty()&&(v.setItem(e+"draft",t.getContent({format:"raw",no_events:!0})),v.setItem(e+"time",(new Date).getTime().toString()),t.fire("StoreDraft"))}function f(t){var e=n(t);u(t)&&(t.setContent(v.getItem(e+"draft"),{format:"raw"}),t.fire("RestoreDraft"))}function c(t){t.undoManager.transact(function(){f(t),g(t)}),t.focus()}function m(r){return function(t){function e(){return t.setDisabled(!u(r))}return t.setDisabled(!u(r)),r.on("StoreDraft RestoreDraft RemoveDraft",e),function(){return r.off("StoreDraft RestoreDraft RemoveDraft",e)}}}var t=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=function(t){return void 0===t},l=tinymce.util.Tools.resolve("tinymce.util.Delay"),v=tinymce.util.Tools.resolve("tinymce.util.LocalStorage"),d=tinymce.util.Tools.resolve("tinymce.util.Tools"),g=function(t,e){var r=n(t);v.removeItem(r+"draft"),v.removeItem(r+"time"),!1!==e&&t.fire("RemoveDraft")},y=tinymce.util.Tools.resolve("tinymce.EditorManager");t.add("autosave",function(t){var e,r,n,a;return t.editorManager.on("BeforeUnload",function(t){var e;d.each(y.get(),function(t){t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&t.getParam("autosave_ask_before_unload",!0)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))}),e&&(t.preventDefault(),t.returnValue=e)}),n=e=t,a=o(n.getParam("autosave_interval"),"30s"),l.setEditorInterval(n,function(){s(n)},a),e.ui.registry.addButton("restoredraft",{tooltip:"Restore last draft",icon:"restore-draft",onAction:function(){c(e)},onSetup:m(e)}),e.ui.registry.addMenuItem("restoredraft",{text:"Restore last draft",icon:"restore-draft",onAction:function(){c(e)},onSetup:m(e)}),t.on("init",function(){t.getParam("autosave_restore_when_empty",!1)&&t.dom.isEmpty(t.getBody())&&f(t)}),r=t,{hasDraft:function(){return u(r)},storeDraft:function(){return s(r)},restoreDraft:function(){return f(r)},removeDraft:function(t){return g(r,t)},isEmpty:function(t){return i(r,t)}}})}();

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