Compare commits

...
Author SHA1 Message Date
mind-one005 be14fe2603 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-22 09:06:39 +09:00
mind-one005 d7f101ee30 wr: 시도,시군구 전체"0" 2건 조회현상 수정 2026-09-21 17:51:09 +09:00
mind-one001 35b9f813c4 로그삭제 2026-09-21 15:08:07 +09:00
mind-one005 aafb326662 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-21 14:46:02 +09:00
mind-one005 aa1b309b26 wr: 중수도 단위, 모달 조회 순서, 시설구분 조회 2026-09-21 14:45:50 +09:00
kiminchan bf0aef59cd Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs/sn/dev_kic 2026-09-21 10:06:26 +09:00
kiminchan b2ddcd0cf3 sn : 통수위험 결과 안내문에 위험 판정기준 및 상태 색상 표시 2026-09-21 09:55:55 +09:00
mind-one001 49ace44c9b 테스트 파일 삭제 2026-09-18 16:35:21 +09:00
mind-one005 91576f401b sn: 침수예측오버레이 수정 2026-09-17 15:41:08 +09:00
mind-one005 febcd18eda Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-17 14:05:11 +09:00
mind-one005 8cc9f0d291 sn: 침수예측현황 침수위험도 수정 2026-09-17 14:04:51 +09:00
kiminchan a8f2c8f41a GIS 편집 신규 관로 미리보기 표시 보완 2026-09-16 17:32:02 +09:00
mind-one001 4d685d5a67 Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs/sn/dev 2026-09-16 11:15:30 +09:00
mind-one001 96196229f4 fix(sn): align business form limits with standardized database 2026-09-16 11:15:18 +09:00
mind-one005 94b2e84c10 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-15 16:28:23 +09:00
mind-one001 80d2230c9a 불필요 파일 삭제 2026-09-15 10:00:42 +09:00
mind-one005 d5f0c3bc95 sn: 침수발생이력, 침수예측현황 팝업크기 변경 외 2026-09-11 11:01:55 +09:00
mind-one001 b7134cb3e4 노후관로현황 그리드 수정 2026-09-10 14:06:24 +09:00
mind-one001 ec78658e46 시도별 노후관로현황 그리드 수정 2026-09-10 14:01:58 +09:00
mind-one001 44361af392 그리드 css 통합 2026-09-10 13:51:42 +09:00
mind-one001 76130caa6d Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-10 11:30:58 +09:00
mind-one001 352a78a0c5 gis 업로드 오류 기능 수정 2026-09-10 11:30:52 +09:00
kiminchan 65bee9586d 도시침수지도 목록 이동 운영 보완 기록 2026-09-09 16:45:12 +09:00
mind-one001 31cda8579a Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-09 15:42:53 +09:00
mind-one001 1dc40b9efc 빈 레이어 트리로 지도만 재사용하는 화면에서는 패널 외곽과 제목도 표시하지 않는다. 2026-09-09 15:42:31 +09:00
mind-one005 6cb3d8ad65 Merge branch 'iipbs/wr/dev' into iipbs/sn/dev 2026-09-09 14:06:31 +09:00
LEEKYUHYUN 225e2732d1 사이드패널 패딩 조정 및 스크롤바 수정 2026-09-09 11:35:59 +09:00
mind-one005 d82ae99b6a sn: 오류 수정 2026-09-09 09:09:09 +09:00
mind-one005 23341b93c7 sn: 침수흔적심선/위선 화살표 연속 scroll 추가 2026-09-08 18:15:40 +09:00
mind-one005 b99f4904b0 sn: featureClickEventPopup에서 침수흔적도 관련 분리 2026-09-08 17:13:36 +09:00
mind-one005 f742feca4b sn: 상단 주석 외 침수흔적도 관련 수정 2026-09-08 15:12:40 +09:00
mind-one005 569e003827 sn: 팝업 화면 여백 조정 2026-09-08 11:01:53 +09:00
mind-one005 2db5dd59af Merge branch 'iipbs/wr/dev' into iipbs/sn/dev 2026-09-07 18:15:04 +09:00
mind-one005 8a8504dca9 sn: 운영정보 버튼 위치 수정 2026-09-07 17:51:36 +09:00
LEEKYUHYUN 7e82c6a802 "디지털하수관로" UI 수정 2026-09-07 17:44:46 +09:00
mind-one005 bdc6251474 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-07 17:01:46 +09:00
mind-one005 479cf5c4a0 sn: 다단header 묶음 처리 외 2026-09-07 16:56:35 +09:00
mind-one001 2b3502161c 시설물 상세창 깨짐 수정 2026-09-04 17:16:32 +09:00
mind-one001 0544a72011 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-04 15:27:18 +09:00
mind-one001 34ba09ce1b 준설관리 기능 보완 2026-09-04 15:24:17 +09:00
mind-one005 aa5a71ae68 sn: 침입수, RDII 제목란 조금 넓게 외 2026-09-04 14:52:11 +09:00
mind-one005 9067e645b3 sn: 정보검색 기본정보 구분란 화면이 줄었을 때, 디자인 외 2026-09-04 14:33:25 +09:00
mind-one005 10d3fc096a sn: 시도 전체일 때, 시군구 목록 비우기 2026-09-04 11:00:38 +09:00
mind-one005 7fd95ba6f9 sn: src/shared/util/util.ts 적용 외 2026-09-04 10:32:36 +09:00
mind-one005 e707637c88 수정 2026-09-03 17:10:11 +09:00
mind-one005 5da89e8ee7 sn: appendApiSearchParams() 함수 적용 2026-09-03 17:09:58 +09:00
mind-one005 f9ff279895 Merge branch 'iipbs/sn/dev' into iipbs/wr/dev 2026-09-02 18:18:01 +09:00
mind-one005 4cde9b67c1 sn: util.ts 추가 정리 2026-09-02 18:10:50 +09:00
mind-one005 80d93c1631 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-01 18:14:16 +09:00
mind-one005 872b3d08c0 sn: 선택 레이어 전체 디자인 수정 2026-09-01 18:10:31 +09:00
kiminchan cb191817a8 Merge branch 'iipbs/sn/dev' into iipbs/sn/dev_kic 2026-09-01 16:01:37 +09:00
kiminchan 8198fda45b 공통코드 조회 기반 업무 화면 표시 적용 2026-09-01 15:55:02 +09:00
mind-one005 37374cb754 sn: 선택 레이어 설정 - 침수흔적도 2026-09-01 15:38:25 +09:00
mind-one005 9e36edec7f sn: 침수흔적위선/심선 정보 닫을 때 지도처리 2026-09-01 11:10:27 +09:00
mind-one005 3204bba71b Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-08-31 23:17:20 +09:00
mind-one005 3cdcf6a6f7 sn: 침수흔적위선 심선/위선 모두 나오도록 2026-08-31 23:16:57 +09:00
kiminchan 922861c872 통수위험 행 선택 시 초단기예보격자망 자동 활성화 2026-08-31 13:42:57 +09:00
mind-one005 4a3945f1e4 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-08-31 11:36:15 +09:00
mind-one005 a1caa1b860 sn: .env.local 원복 2026-08-31 11:36:05 +09:00
mind-one005 166dea2b86 sn: 침수흔적도 layer 수정 2026-08-31 11:34:19 +09:00
kiminchan 3f7262e213 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-08-31 11:06:24 +09:00
kiminchan 231ee10ff8 조사사업 양식 버튼 병합 충돌 오류 수정 2026-08-31 11:05:22 +09:00
mind-one005 5aa9ab4f39 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-08-31 11:00:02 +09:00
kiminchan 8c46f7ca06 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-08-31 10:46:38 +09:00
kiminchan 23e15daa37 조사사업 등록·수정 양식 다운로드 흐름 보완 2026-08-31 10:35:05 +09:00
mind-one005 dd56ed60ad sn: 용어 변경 - 수위, 깊이 > 위선,심선 2026-08-31 10:23:37 +09:00
mind-one005 18129905d8 sn: 연도버튼 및 침수흔적깊이 2026-08-29 11:21:26 +09:00
mind-one005 d03263a3c6 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-08-28 16:48:17 +09:00
mind-one005 be46bcbd17 sn: 침수흔적깊이 정보 추가 2026-08-28 16:08:47 +09:00
mind-one001 201dbf577f 관망도 업로드 수정 2026-08-28 15:45:43 +09:00
mind-one001 884dd71ca7 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev
# Conflicts:
#	apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx
2026-08-28 10:00:36 +09:00
mind-one001 70efdc9db5 유하방향 표출 수정 2026-08-28 09:52:41 +09:00
mind-one005 36549e7281 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-08-27 22:34:15 +09:00
mind-one005 c1ffc33834 sn: 침수예측지도 오버레이 & 침수흔적깊이 2026-08-27 22:33:37 +09:00
mind-one005 2c3961644c sn: 침수예측지도 오버레이 표시1 2026-08-27 14:10:44 +09:00
mind-one001 2fe3631a0d Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-08-26 16:26:19 +09:00
mind-one001 e0cdba00be 그리드 정렬기능 2026-08-26 16:25:29 +09:00
kiminchan 5e206dd210 Merge branch 'iipbs/sn/dev' into iipbs/sn/dev_kic 2026-08-26 14:44:57 +09:00
kiminchan 2008a5a9ee 통수위험 지도 안내 및 과거이력 조회 동작 보완 2026-08-26 14:43:27 +09:00
mind-one005 53aeeccc2a Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-08-26 13:11:52 +09:00
mind-one005 c7738a6d6c sn: 침수예측현황 지도이동시 도시침수지도 layer점검 2026-08-26 13:11:40 +09:00
kiminchan baf29df950 통수위험 매핑 시설 지도 표출 및 과거이력 행 식별 보완 2026-08-26 13:07:01 +09:00
mind-one005 0a13f34092 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-08-26 11:11:30 +09:00
mind-one005 0b45361135 sn: 침수예측현황 팝업에서 지도이동 2026-08-26 11:11:18 +09:00
kiminchan 83420c76b8 Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs/sn/dev_kic 2026-08-26 10:38:51 +09:00
mind-one005 42f83bcd24 sn: 침수예측현황 목록 항목 순서 변경 2026-08-26 10:06:55 +09:00
mind-one001 ccfbe6ae32 팝업 병합 그리드 기능 추가 2026-08-25 16:43:07 +09:00
mind-one001 8126fb844a Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-08-25 15:30:01 +09:00
mind-one001 23c9ddf4ce ui 버튼 동작 통일 2026-08-25 15:29:55 +09:00
mind-one005 63388ccc90 dm: env 수정 2026-08-24 17:45:53 +09:00
LGS bda3228d0f 물 재이용 소개 / 물 재이용 현황 [이미지 수정 및 텍스트 수정] 2026-08-24 16:57:15 +09:00
LGS 95ca40913f 물 재이용 소개[우리나라의 수자원 현황 및 물이용 여건] 이미지 교체 2026-08-24 16:16:41 +09:00
kiminchan 8bfe1c9ab2 강우관측소 지도 클릭 비활성화 2026-08-24 11:23:40 +09:00
kiminchan 0dc981819f 강우 레이어 프록시와 관측소 아이콘 추가 2026-08-21 17:22:25 +09:00
mind-one001 4b7ada9362 Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs/sn/dev
# Conflicts:
#	apps/sn/app/(map-layout)/map/components/waterRisk/WaterRiskPopupPage.tsx
#	apps/sn/src/features/windowPopup/model/popupDefinitions.ts
2026-08-20 17:16:33 +09:00
mind-one001 71c5dfb8dd fix(sn): 조사관리 팝업 레이아웃과 등록 흐름 개선 2026-08-20 17:14:07 +09:00
mind-one001 7e3911dbd6 feat(sn): 침수예측 및 통수위험 팝업 UI 보완 2026-08-20 17:13:54 +09:00
kiminchan 839d31b9ca 통수위험 결과조회 공통 그리드 및 지도 표출 보완 2026-08-20 17:09:33 +09:00
mind-one001 386fdade59 Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs/sn/dev 2026-08-20 15:10:36 +09:00
mind-one001 0cbbc3b934 feat(sn): CS 기준 UI 통일 및 지도 팝업 표준화 2026-08-20 14:57:20 +09:00
kiminchan f5051225ea 통수위험 결과조회 및 지도 시각화 프로토타입 구현 2026-08-18 17:28:48 +09:00
mind-one005 f7218fc98a sn: 침수예측현황-단위 엑셀다운로드 수정 2026-08-18 13:31:22 +09:00
mind-one001 c9e3ff0d91 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-08-12 11:14:05 +09:00
mind-one001 474ad1ab26 GIS관리 기능 보완 2026-08-12 11:13:49 +09:00
mind-one005 099e000f88 sn: 분석도구 - 침수예측현황 2026-08-11 15:09:30 +09:00
mind-one005 7fa4538667 sn: 도시침수예방사업 다운로드 header 수정 2026-08-11 14:34:47 +09:00
mind-one005 91e11a7857 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-08-11 14:33:20 +09:00
mind-one001 d582293e5f 보수이력 목록 조회 항목 추가 2026-08-11 11:28:52 +09:00
mind-one001 d8c5344451 관망도 분석도구 위치 조정 2026-08-11 11:11:09 +09:00
mind-one001 d23a16905d Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-08-11 10:32:45 +09:00
mind-one001 2649ce286b 보수이력 기능 개발 2026-08-11 10:32:35 +09:00
mind-one005 2f9e97b65b sn: 도시침수예방사업01 2026-08-10 22:04:36 +09:00
mind-one005 e1940043e9 sn: 노후 하수관로 정비사업 등록/수정 입력란 수정 2026-08-07 22:49:44 +09:00
mind-one005 7f1c804a0c dm: LoginModal 사업자등록번호 입력 수정 2026-08-07 20:59:05 +09:00
mind-one005 d07bb6074d sn: 노후하수관로정비사업 시군구 필수 아님 2026-08-07 18:22:08 +09:00
mind-one005 4109691eea Merge branch 'iipbs/sn/dev_ljp' into iipbs/sn/dev 2026-08-07 15:58:28 +09:00
mind-one005 4eaac5dbf1 sn: 노후하수관로정비사업 - 팝업 2026-08-07 15:57:14 +09:00
mind-one001 b99fe8daf1 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-08-07 15:28:49 +09:00
mind-one001 3f4e3581dd 준설관리 개발완료 2026-08-07 15:28:33 +09:00
mind-one005 f00627270f sn: 최종확정예산사업 popup 2026-08-06 18:40:19 +09:00
mind-one005 1858739bb9 sn: 노후하수관로정비사업 조회조건 유지 2026-08-06 10:46:47 +09:00
kiminchan 3d320d0e19 맨홀 선택 시 기존 심볼이 유지되도록 보완 2026-08-06 09:42:41 +09:00
kiminchan 9d2c0afb98 조사 지도 조건과 노후관로 범례 보완 2026-08-06 09:14:56 +09:00
mind-one001 29f2a9169e Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev
# Conflicts:
#	apps/sn/src/features/windowPopup/model/popupDefinitions.ts
#	apps/sn/src/features/windowPopup/registry.tsx
2026-08-05 16:24:52 +09:00
mind-one001 493acc9ebd 통수능/준설관리 2026-08-05 16:05:07 +09:00
mind-one005 d3bdc58994 sn: 순번 삭제 외 2026-08-05 15:49:55 +09:00
kiminchan 20efccb4d9 Merge branch 'iipbs/sn/dev_kic' into iipbs/sn/dev 2026-08-05 13:58:16 +09:00
kiminchan f58daf3bac 침수지도 목록보기 및 WMS 표출 구현 2026-08-05 13:55:22 +09:00
mind-one005 b42810d29a sn: 노후하수관로정비사업1 2026-08-04 20:30:26 +09:00
mind-one005 2e6a75141b sn: 노후 하수관로 정비사업 2026-08-03 18:16:42 +09:00
kiminchan ce60e5f5fb Merge branch 'iipbs/sn/dev' into iipbs/sn/dev_kic 2026-08-03 10:12:47 +09:00
kiminchan c9d85a8ce4 조사결과 관로·맨홀 지도 레이어와 보고서 조회 연동 2026-08-03 10:11:48 +09:00
mind-one001 0f8b6cd6d0 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev
# Conflicts:
#	apps/sn/app/(map-layout)/map/components/gisManagement/GisManagementPopupPage.tsx
#	apps/sn/app/(map-layout)/map/components/waterRisk/WaterRiskPopupPage.tsx
2026-07-30 17:26:41 +09:00
mind-one001 b11658e1d6 준설관리 초안 2026-07-30 17:18:59 +09:00
kiminchan bcf5d3caec 조사관리 팝업 UI와 관로 상세 등급 표시 보정 2026-07-30 16:53:30 +09:00
kiminchan 474676af17 조사 상세현황 배수방식·관종 명칭 표시 2026-07-30 13:17:16 +09:00
kiminchan 3cc0e9de14 조사 상세조회 및 집계 엑셀 다운로드 연동 2026-07-30 10:53:32 +09:00
kiminchan ff7be74437 Merge branch 'iipbs/sn/dev' into iipbs/sn/dev_kic 2026-07-29 13:31:01 +09:00
kiminchan f52f22e80f 조사결과 검토·저장 및 사업보고서 목록 표시 개선 2026-07-29 13:22:59 +09:00
mind-one005 86e00d48dc sn: 지도 침수흔적도 우측 정보에 강우분석 추가 2026-07-29 11:43:51 +09:00
mind-one005 bf61f6f76a sn: 침수발생이력 팝업 수정 2026-07-28 17:51:47 +09:00
kiminchan 62c41297c4 조사사업 수정 및 조사양식 다운로드 연동 2026-07-28 13:28:15 +09:00
mind-one005 09b5cc3798 sn: 시도별 노후관로현황 2026-07-27 17:51:33 +09:00
kiminchan 155a727a77 처리분구 인라인 편집 및 저장 후 목록 동기화 2026-07-27 14:18:02 +09:00
kiminchan a52dacf500 조사사업 등록의 사업차수 입력과 행정구역 배치 보완 2026-07-27 11:29:48 +09:00
mind-one005 e8e38310ee Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-07-27 10:32:25 +09:00
mind-one005 2652e2cfe0 sn: components.css 통합 2026-07-27 10:29:45 +09:00
kiminchan 5a76ae8a88 Merge branch 'iipbs/sn/dev' into iipbs/sn/dev_kic 2026-07-27 10:20:27 +09:00
kiminchan 7d9b97aac9 조사상세 본단위구간 모드 및 결함별 엑셀 다운로드 구현 2026-07-27 10:15:06 +09:00
mind-one005 69c3183e35 sn: 침수흔적도 지도표출 2026-07-24 18:13:54 +09:00
mind-one005 5e6477cddd Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-07-24 15:23:41 +09:00
mind-one005 c13cab394a sn: 침수발생이력 변수명 수정 2026-07-24 15:23:35 +09:00
kiminchan 73f1940cb0 Merge branch 'iipbs/sn/dev' into iipbs/sn/dev_kic 2026-07-24 15:12:08 +09:00
mind-one005 8a35d232ef sn: 침수발생이력 팝업 api 적용 2026-07-24 14:22:41 +09:00
kiminchan 728981cfe6 조사 결함 분류별 상세보기 및 등록 팝업 개선 2026-07-24 14:08:25 +09:00
kiminchan 11cb2b9044 조사사업 관로·맨홀 상세보기 팝업 구현 2026-07-24 13:05:26 +09:00
kiminchan c1d6cd7b8e 조사사업 보고서 다운로드 및 상세현황 그리드 보정 2026-07-23 17:26:08 +09:00
kiminchan 477426c384 조사사업 등록·결과 업로드 및 관로·맨홀 종합집계 화면 구현 2026-07-23 17:06:01 +09:00
kiminchan ec60a05b01 처리분구관리 화면 구현 2026-07-22 13:15:06 +09:00
mind-one005 90f0e4691b Merge branch 'iipbs/wr/dev' into iipbs/sn/dev 2026-07-21 21:03:19 +09:00
mind-one005 b41a317788 wr: fo: sn: 지도화면 session confirm 2026-07-21 17:59:15 +09:00
mind-one005 559e4ee352 Merge remote-tracking branch 'origin/main' into iipbs/wr/dev 2026-07-21 11:38:33 +09:00
jcchoi a1d43dc65c 차수 단어 제외 2026-07-21 11:11:55 +09:00
chanii8 d26cd90b02 [common/components] feat: nofooter에 authService props 추가 2026-07-21 09:20:37 +09:00
chanii8 45f72f7333 [common/components] chore: auth-header 참조 경로 수정 2026-07-20 18:42:15 +09:00
chanii8 196d31a084 [common/components] feat: raw fetch 헤더에 토큰 주입 2026-07-20 18:35:14 +09:00
chanii8 2f0448a98e [common/components] refactor: banner, menu, site를 기본 fetch 로 수정 2026-07-20 17:08:36 +09:00
kiminchan 5e0693e057 Merge branch 'iipbs/sn/dev_kic' into iipbs/sn/dev 2026-07-20 15:33:27 +09:00
kiminchan 1708555133 시설물 레이어 체크 시 스크롤 위치 유지 2026-07-20 15:29:23 +09:00
mind-one005 c38d95fd60 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-07-20 15:03:52 +09:00
kiminchan 667f7b2094 Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs/sn/dev_kic 2026-07-20 14:57:39 +09:00
kiminchan d17909df41 그리드 공통 정렬 기능 확대 적용 2026-07-20 14:56:23 +09:00
mind-one005 88113fca1c wr: fo: sn: NEXT_PUBLIC_COOKIE_COMAIN 개발 추가 2026-07-20 14:52:48 +09:00
chanii8 81fc947c9d chore: as, st 앱 standalone 활성 2026-07-20 13:49:22 +09:00
chanii8 c684440e65 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-07-20 13:45:21 +09:00
chanii8 4c6222acf1 Merge branch 'iipbs/cs/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-07-20 13:45:12 +09:00
chanii8 2131ecc315 Merge branch 'iipbs/cm/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-07-20 13:45:07 +09:00
chanii8 f53c72f31f Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/as/dev 2026-07-20 13:44:16 +09:00
chanii8 1d3910c157 [st] chore: route handler 쿠키 도메인 설정 구문 수정 2026-07-20 13:43:46 +09:00
chanii8 3b2f5c2d5d [cm] chore: route handler 쿠키 도메인 설정 구문 수정 2026-07-20 13:42:55 +09:00
chanii8 903cdaad45 [as] chore: route handler 쿠키 도메인 설정 구문 수정 2026-07-20 13:41:15 +09:00
chanii8 386fc61a3f [cs] chore: route handler 쿠키 도메인 설정 구문 수정 2026-07-20 13:39:43 +09:00
dongsilove 6e354690c5 sn: NEXT_PUBLIC 제거 2026-07-19 23:52:05 +09:00
dongsilove adae670318 Merge branch 'iipbs/wr/dev' into iipbs/sn/dev 2026-07-19 23:03:36 +09:00
dongsilove bdbefbf600 :wr :fo :dm NEXT_PUBLIC 삭제 2026-07-19 20:40:41 +09:00
dongsilove 939089a79c :wr :sn :fo NEXT_PUBLIC_COOKIE_DOMAIN 추가 2026-07-19 18:14:34 +09:00
dongsilove f8354344ff Merge branch 'iipbs/wr/dev' 2026-07-18 14:02:49 +09:00
dongsilove e596c1f0a6 wr: sn: fo: X-Device-Id 추가 2026-07-18 14:01:33 +09:00
chanii8 7f53badcf3 chore: auth service 수정 2026-07-16 18:33:35 +09:00
chanii8 1dcf01bb0e chore: 의존성 업데이트 2026-07-16 18:26:58 +09:00
chanii8 eec25ab1c4 Merge branch 'iipbs/st/dev' 2026-07-16 18:26:24 +09:00
chanii8 f90f093385 Merge branch 'iipbs/cs/dev' 2026-07-16 18:26:13 +09:00
chanii8 befe56fd44 Merge branch 'iipbs/cm/dev' 2026-07-16 18:26:02 +09:00
chanii8 47d91424c5 Merge branch 'iipbs/as/dev' 2026-07-16 18:25:49 +09:00
chanii8 ddd72f71d2 [st] feat: 로그인 시 X-Device-Id 헤더 추가 2026-07-16 18:24:39 +09:00
chanii8 1a1ff2ec30 [cs] feat: 로그인 시 X-Device-Id 헤더 추가 2026-07-16 18:22:58 +09:00
chanii8 10d89279bc [cm] refactor: auth service 수정 2026-07-16 18:21:37 +09:00
chanii8 a6f9ba7526 [as] feat: 재원협의 보완요청 로직 작업 2026-07-16 17:54:57 +09:00
mind-one005 19dfe45b0b wr: sn: fo: BaseLayout 호출 param 추가 2026-07-16 16:58:26 +09:00
chanii8 f584e21480 [as] feat: 설치인가 보완요청 및 완료 기능 구현 2026-07-16 15:17:43 +09:00
dongsilove 6acacbfcf4 sn: 공통 모듈 기반 로그인 변경 2026-07-15 22:04:26 +09:00
mind-one005 52a79eca60 fo: 공통 모듈 기반 로그인 변경 2026-07-15 18:22:39 +09:00
chanii8 70faadb27e [as] feat: 보완요청 기능 구현 2026-07-15 17:52:43 +09:00
mind-one005 2eb1809f36 wr: 공통 모듈기반 로그인 변경 2026-07-15 17:22:51 +09:00
kiminchan d9febf8010 하수관로 편집 세션 UI 및 토글 동작 개선 2026-07-15 15:54:15 +09:00
kiminchan d4c5787360 하수관로 편집 세션 배치 및 팝업 전환 개선 2026-07-15 15:39:38 +09:00
mind-one005 c8c461e8e8 Merge remote-tracking branch 'origin/main' into iipbs/wr/dev 2026-07-15 14:54:42 +09:00
chanii8 9e2da003f1 [as] feat: 전결 로직 수정 및 결재현황 섹션 구현 2026-07-15 13:36:20 +09:00
rsone001 ae41483bb7 [ST] - fix: 집계 컬럼 설정 fixedColumns 수정 2026-07-15 11:39:09 +09:00
kiminchan d062b8a7d4 지반침하 연계 관로 탭의 부모 종속 제거 처리 2026-07-15 11:34:06 +09:00
rsone001 18825dd0c3 [ST] - DataGrid setRow 로직 수정 및 간단한 수정사항 처리 2026-07-15 11:29:49 +09:00
mind-one005 6b2ea58b66 wr: Password 초기화 2026-07-15 10:26:56 +09:00
kiminchan fdfb37d4d0 지반침하 지도 UX 및 목록 표시 개선 2026-07-14 16:57:51 +09:00
mind-one005 450de39f8c dm: 로그인 변경 2026-07-14 16:48:08 +09:00
chanii8 c87ad8ea1b [as] refactor: 결재선 선택 다이얼로그 통합 2026-07-14 15:03:45 +09:00
mind-one005 b336651503 dm: 등록시 비번 해쉬처리 2026-07-14 14:50:54 +09:00
chanii8 7a1949fc6f [as] feat: 결재선 수정 사항 반영 2026-07-14 12:28:22 +09:00
rsone001 35a63d7c5c [st] - fix: 지난년도 데이터 가져오기 button confirm 추가 2026-07-14 11:54:29 +09:00
kiminchan 031f5e2ed8 하수관망도 시도 단위 처리시설 조회 지원 2026-07-14 10:18:46 +09:00
kiminchan 189b4b7599 지반침하 연계 탭 유형 구분·관로 단일화·부모 복귀 기능 및 후속 정책 반영 2026-07-14 09:55:06 +09:00
dongsilove 6ada32cb9f Merge branch 'iipbs/wr/dev' 2026-07-13 18:24:54 +09:00
chanii8 5491c524c5 [as] feat: 멀티 디바이스 로그인 구현 2026-07-13 18:08:26 +09:00
chanii8 8d29f25101 [as] feat: 체크리스트 전체선택 기능 추가 2026-07-13 16:43:01 +09:00
kiminchan 28d4b2d831 RDII 분석 진행률 표시 및 결과 팝업 가독성 개선 2026-07-13 15:20:04 +09:00
chanii8 399645a80a [as] feat: 설계자 전화번호 검증 코드 추가 2026-07-13 14:43:55 +09:00
chanii8 9d13809d01 [as] feat: 체크리스트 항목 관리에 '정렬 순서' 컬럼 추가 2026-07-13 14:28:10 +09:00
chanii8 e8c8db7383 [as] chore: 불필요한 코드 제거 2026-07-13 14:05:25 +09:00
chanii8 b41814ed0c [as] chore: 체크리스트 정렬 코드 제거 (서버에 위임) 2026-07-13 12:01:17 +09:00
chanii8 d71f2043de [as] feat: 필수 첨부 파일 업로드 가능한 확장자 제한 2026-07-13 11:38:38 +09:00
kiminchan 87ba3e8fa2 침입수 분석 전역 프로그래스바 표시와 상태 polling 연동 마감 2026-07-13 11:26:15 +09:00
chanii8 eabbbf456f Merge branch 'iipbs/cm/dev' 2026-07-13 11:10:46 +09:00
chanii8 ebfba7ac8c [cm] chore: 프로젝트 설정 수정 2026-07-13 11:07:32 +09:00
mind-one005 fe4a4a420e wr: EMFILE 오류수정 2026-07-13 10:51:16 +09:00
rsone001 14ae432833 [st] - fix: 입력단 superAdmin 필드 존재 시에만 추가되도록 수정 2026-07-10 12:27:52 +09:00
kiminchan 009a38f3ca RDII 분석 완료 상태 polling 및 전역 완료 alert 적용 2026-07-09 18:51:01 +09:00
chanii8 cfc73e4094 Merge branch 'iipbs/as/dev' 2026-07-09 17:53:18 +09:00
chanii8 748be6faa5 [as] chore: 파비콘 파일 추가 2026-07-09 17:51:20 +09:00
chanii8 330e2e5c69 Merge remote-tracking branch 'origin/iipbs/as/dev' 2026-07-09 17:39:35 +09:00
chanii8 343a3f81bf [as] feat: 설치인가 전자결재 로직 수정 2026-07-09 17:39:03 +09:00
mind-one005 13c7d5a0c5 wr: 중수도시설 등록 페이징 수정 2026-07-09 17:38:57 +09:00
kiminchan a9c38b356a 침입수 즉시 제출 alert 복구 및 전역 완료 alert 감시 추가 2026-07-09 17:30:21 +09:00
kiminchan 4181257afd 하수관망도 검색바 한글 깨짐 복구 2026-07-09 16:23:21 +09:00
chanii8 0141ed56c2 [as] feat: 재원협의 전자결재 로직 수정 2026-07-09 16:17:51 +09:00
kiminchan 35dc205810 Merge branch 'iipbs/sn/dev_kic' into iipbs/sn/dev 2026-07-09 16:12:21 +09:00
mind-one005 d10a17eaf6 sn: 테마변경 1차 2026-07-09 15:57:30 +09:00
mind-one005 a623c53fa5 wr: css ㅍ 2026-07-09 15:39:48 +09:00
kiminchan 3db8babee7 침입수·RDII 분석 대상지역 기본값을 양평으로 설정 2026-07-09 13:03:21 +09:00
kiminchan ca7ac992e2 침입수 분석결과 실측하수량 관로 우선 줌인 및 지역 선이동 제거 2026-07-09 11:37:26 +09:00
rsone001 a956c9894e [st] - 공지사항 메뉴 새창으로 열도록 return 수정 2026-07-09 11:35:50 +09:00
kiminchan 6c84ba6aeb 침입수·RDII 분석 화면 결과보기 버튼 및 지도결과 popup 연계 2026-07-09 10:45:53 +09:00
mind-one005 e99b7d8162 Merge branch 'iipbs/wr/dev' 2026-07-09 10:01:38 +09:00
mind-one005 09bb613929 wr: 오류수정 2026-07-09 10:00:20 +09:00
mind-one005 761b0dd502 wr: 수요처팝업 비번 초기화 2026-07-09 09:56:41 +09:00
mind-one005 8c72cb3b72 wr: 하수재이용실적 등록 2026-07-08 20:56:51 +09:00
chanii8 35b6f4ae54 [as] feat: 결재선 저장 api 수정 2026-07-08 19:22:40 +09:00
chanii8 cf5ade639a Merge remote-tracking branch 'origin/iipbs/cm/dev' 2026-07-08 18:52:53 +09:00
chanii8 0dfd5efa97 [cm] feat: auth 서비스 수정 및 jwt 기반 로그인 만료 구현 2026-07-08 18:52:30 +09:00
chanii8 e6cbb9dfd8 Merge branch 'iipbs/st/dev' 2026-07-08 18:21:36 +09:00
chanii8 52c7215cd8 Merge branch 'iipbs/cs/dev' 2026-07-08 18:21:28 +09:00
chanii8 fe88732a80 Merge branch 'iipbs/as/dev' 2026-07-08 18:21:22 +09:00
chanii8 aa5f85a3c1 [st] remove: 로그아웃 라우트 핸들러 제거 2026-07-08 18:21:00 +09:00
chanii8 843519c3b5 [as] remove: 로그아웃 라우터 핸들러 제거 2026-07-08 18:17:03 +09:00
chanii8 c74917d1d9 [cs] remove: 로그아웃 라우터 핸들러 제거 2026-07-08 18:16:24 +09:00
chanii8 9dca199d78 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-07-08 18:05:51 +09:00
chanii8 4fa7542aca [st] feat: 통계 검증 리포트 반려 처리 로직 수정 2026-07-08 18:05:35 +09:00
chanii8 1cd24b5dd5 [st] chore: stClient 참조 경로 수정 2026-07-08 18:05:11 +09:00
rsone001 34f1bf5e6a Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-07-08 17:45:01 +09:00
rsone001 675e5f58d1 [st] - fix log detail 로직 수정 2026-07-08 17:39:35 +09:00
mind-one005 127fb41145 dm: 수요신청현황 api 수정, 수정시 비번 처리,무한루프처리 2026-07-08 17:05:57 +09:00
chanii8 8abf32bb53 Merge branch 'iipbs/as/dev' 2026-07-08 16:04:15 +09:00
chanii8 b567e5df13 [as] remove: instl-sncn-old 페이지 제거 2026-07-08 16:03:16 +09:00
chanii8 40acdafed2 Merge remote-tracking branch 'origin/iipbs/cs/dev' 2026-07-08 16:00:49 +09:00
chanii8 7f6b05e4e5 Merge remote-tracking branch 'origin/iipbs/as/dev' 2026-07-08 16:00:38 +09:00
chanii8 0d4a698c11 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-07-08 15:57:24 +09:00
chanii8 5bacfb04b5 [st] chore: auth 서비스 수정 2026-07-08 15:57:17 +09:00
chanii8 1e8514a0bc Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-07-08 15:56:41 +09:00
chanii8 b6714dd8a4 [as] chore: auth 서비스 수정 2026-07-08 15:55:54 +09:00
chanii8 e05c2a03e0 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/as/dev 2026-07-08 15:53:10 +09:00
chanii8 914df505ae [cs] chore: auth 서비스 수정 2026-07-08 15:52:14 +09:00
chanii8 7e59ae6b66 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-07-08 15:46:08 +09:00
chanii8 eb84a23d7e [common] feat: useSessionExtendConfirm 생성 2026-07-08 15:43:42 +09:00
mind-one005 1076120516 dm: 전화번호 포맷 & 비번 변경 2026-07-08 10:32:33 +09:00
dongsilove 54ce26adbc Merge remote-tracking branch 'origin/iipbs/wr/dev' 2026-07-07 20:30:15 +09:00
chanii8 28d54b615b [cs] chore: auth service 수정 2026-07-07 18:19:43 +09:00
chanii8 424fa576fb [cs] feat: auth service 수정 (BaseLayout DI 패턴) 2026-07-07 18:04:08 +09:00
chanii8 7f8e5fe9f9 [as] feat: auth service 수정 (BaseLayout DI 패턴) 2026-07-07 18:02:18 +09:00
chanii8 47f1bd012c Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/as/dev 2026-07-07 17:51:28 +09:00
chanii8 0c2e762cee [cm] chore: next-env.d.ts 파일 깃에서 제거 2026-07-07 17:49:43 +09:00
chanii8 68124e6abb [common/components] chore: BaseLayout 수정 2026-07-07 17:37:26 +09:00
chanii8 5587e6b6a3 [common] feat: 공통 auth 및 components 수정(authService DI 패턴) 2026-07-07 17:34:27 +09:00
rsone001 ac1e98fbaa Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-07-07 17:02:47 +09:00
rsone001 d25f9f3a01 [st] - fix: 2-3 컬럼 로직 수정, 입력단 excel 업로드 시 행정동코드 매핑 검사 2026-07-07 17:01:36 +09:00
mind-one005 fb638752eb wr: 검색어 있는 경우 paging과 건축물용도 2026-07-07 16:42:51 +09:00
chanii8 8c3dd62f6d Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-07-07 15:14:27 +09:00
chanii8 e79635b81a [st] feat: 통계 검증 리포트 페이지 수정 2026-07-07 15:14:18 +09:00
rsone001 e3a9851de6 [st] - fix: 하수관로 관종별 현황, 입력 가능 연도만 매설연장 컬럼 수정가능, 관리자 계정일 시 엑셀 업로드로 모든 cell 수정 가능 2026-07-07 12:17:44 +09:00
mind-one005 34811d4571 Merge branch 'main' into iipbs/sn/dev 2026-07-07 11:03:47 +09:00
chanii8 3167a9a6e3 WIP: 전자결재 로직 수정 2026-07-07 09:18:50 +09:00
mind-one005 a29d1f0cc2 wr: 수요처 pk변경 1 2026-07-06 17:37:28 +09:00
jcchoi e76cddd45b Merge remote-tracking branch 'origin/iipbs/st/dev' 2026-07-06 16:53:09 +09:00
rsone001 3c214f4725 [st] - fix: chat.js registerd error 2026-07-06 14:54:32 +09:00
rsone001 80861938f3 [st] - fix: 입력단 텝 형식 그리드 제출 및 저장 후 재조회 로직 수정 2026-07-06 14:11:25 +09:00
rsone001 401f2dba13 [ST] - SearchFrom 수정 2026-07-06 11:29:56 +09:00
jcchoi c4d7c516ef Merge remote-tracking branch 'origin/iipbs/st/dev' 2026-07-05 10:58:48 +09:00
chanii8 2f11b1185b [as] feat: 재원협의 신청 및 수정 페이지 작업 2026-07-03 17:23:18 +09:00
rsone001 42274a71b7 Merge remote-tracking branch 'origin/feature/sein' into iipbs/st/dev 2026-07-03 16:19:48 +09:00
rsone001 f1fcfa9ee4 [st] - 그리드 style 적용 로직 수정, 로그 조회 일정 선택 로직 수정(최대 1년 제한) 2026-07-03 16:19:27 +09:00
yangsein afcfbbbf61 feat: 집계표 비고 제거 , 행정동 인구관리 갱신 버튼추가 2026-07-03 16:15:34 +09:00
mind-one005 defba2962d wr: 하수재이용시설 등록 수요처 추가 2026-07-03 15:17:10 +09:00
rsone001 4c7666169c Merge remote-tracking branch 'origin/feature/sein' into iipbs/st/dev 2026-07-03 13:09:41 +09:00
yangsein 5580b4301f [feat]집계 11분뇨찌꺼기 tab2 소화조 컬럼 매핑 수정 2026-07-03 13:02:40 +09:00
jcchoi 421ff8cb40 Merge remote-tracking branch 'origin/iipbs/st/dev' 2026-07-02 18:31:43 +09:00
mind-one005 96b95e0eb9 Merge branch 'iipbs/wr/dev' 2026-07-02 18:08:03 +09:00
rsone001 cea2d39df8 [st] - 일정등록, 입력단 지난검증결과 알림 뱃지, 로그 상세조회, 검토단 cell style 적용 2026-07-02 17:46:13 +09:00
kiminchan f4eae0d33e 지반침하 좌표 실패 이슈 및 잔여 케이스 문서 반영 2026-07-02 15:38:45 +09:00
chanii8 716a20cc22 [as] feat: 코드관리 페이지 통합 2026-07-02 15:35:56 +09:00
mind-one005 873a74492a wr: refresh token 적용 2026-07-02 15:33:06 +09:00
kiminchan 516d93d6fa RDII 분석결과 팝업 지도연계 및 결과전용 팝업 적용 2026-07-02 11:38:40 +09:00
mind-one005 958a8ffe2c Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-07-02 09:10:49 +09:00
mind-one005 0286e2a92e Merge branch 'iipbs/wr/dev' 2026-07-02 09:10:14 +09:00
mind-one005 01e339cc61 wr: 하수재이용시설 조회 api 정리2 2026-07-01 18:18:36 +09:00
chanii8 0e7c9b4a96 [as] feat: 재원분담 API 추가 2026-07-01 17:52:57 +09:00
kiminchan d5814043d9 침입수 분석결과 지도연계 및 I/I 분석 메뉴 정리 2026-07-01 17:35:58 +09:00
chanii8 e81210ec04 [as] feat: 재원협의 신청 페이지 작업 및 설치인가 신청 폼 섹션 공통 컴포넌트화 2026-07-01 17:19:48 +09:00
mind-one005 8d17623d3a wr: 하수재이용시설 조회 api 정리1 2026-07-01 15:18:42 +09:00
jcchoi ba9d7c8172 Merge remote-tracking branch 'origin/iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 3m9s
2026-07-01 10:13:30 +09:00
mind-one005 06f5a3cde0 wr: 하수재이용실적 등록1 2026-06-30 18:14:19 +09:00
rsone001 25417dcda1 [st] - any type 수정, 검토 그리드 cell style 수정 2026-06-30 17:59:11 +09:00
jcchoi 9a3b332130 Merge remote-tracking branch 'origin/iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 3m57s
2026-06-30 16:02:29 +09:00
rsone001 1236a24b61 [st] - fix: 입력단 날짜 데이터 값 없을 시 null 처리 2026-06-30 15:56:47 +09:00
rsone001 f50de743e3 [st] - feat 통계인구관리 기능 추가 2026-06-30 14:56:52 +09:00
rsone001 0c2568aad0 Merge remote-tracking branch 'origin/feature/sein' into iipbs/st/dev 2026-06-30 14:40:51 +09:00
yangsein b15cbeb568 feat: 행정동인구 탭 추가 2026-06-30 14:36:25 +09:00
rsone001 afdc06638f [st] - 검토단 작업 완료 및 입력6-1, 10-1 운영데이터업데이트 기능 추가, 텝 영역 우클릭 후 텝 닫기 기능 추가 2026-06-30 14:23:08 +09:00
chanii8 6fb610d9aa [as] feat: fnrs-cnsltn-mng 서비스 구현 2026-06-30 13:51:22 +09:00
kiminchan c14d536133 정보검색 기본계획보고서 탭 조회 UX 개선 2026-06-30 13:09:07 +09:00
chanii8 3008ae2eeb Merge branch 'iipbs/as/dev'
Deploy Frontend / deploy (push) Successful in 3m41s
2026-06-30 12:27:39 +09:00
chanii8 3b4d9df0e8 [as] feat: 설치인가 수정 페이지 수정 2026-06-30 12:27:13 +09:00
mind-one005 48d4a5e135 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-06-30 09:05:23 +09:00
mind-one005 ff33565513 wr: 지도 overlay 버튼 수정 2026-06-29 18:18:48 +09:00
chanii8 b40bae1b50 Merge branch 'iipbs/as/dev'
Deploy Frontend / deploy (push) Successful in 5m53s
2026-06-29 18:00:17 +09:00
chanii8 0533d229e6 [as] feat: 설치인가 검토의견 작성 폼 추가 2026-06-29 17:59:18 +09:00
mind-one005 bbb755bf56 wr: 오류 수정 2026-06-29 17:46:17 +09:00
kiminchan 31caee87db GIS 편집 임시 관로 스냅 지원 및 세션 취소/뒤로가기 분리 2026-06-29 17:45:31 +09:00
kiminchan 87cda451f3 지반침하 설정 패널 연도선택 UI 및 우측 탭 스타일 정리 2026-06-29 17:08:31 +09:00
mind-one005 e2b8f901a3 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-06-29 16:54:03 +09:00
mind-one005 951e2c1f89 wr: 지도 팝업 제목 style 통일 2026-06-29 16:53:43 +09:00
chanii8 20fd291cc3 [as] feat: 설치인가 신청 시 업무구분 선택 팝업 추가 2026-06-29 16:08:21 +09:00
kiminchan ee3ddce41d 지반침하 상세 관로 용도 OGC011 매핑 및 표 너비 조정 2026-06-29 14:51:46 +09:00
chanii8 fb160e712b [as] chore: 설치인가 페이지 수정 2026-06-29 14:46:40 +09:00
mind-one005 9a6cc1779f wr: 지도 목록보기 다운로드할 데이터 ... 오류 수정 2026-06-29 14:39:52 +09:00
chanii8 b459d0fbcb Merge branch 'iipbs/as/dev'
Deploy Frontend / deploy (push) Successful in 8m47s
2026-06-29 13:50:05 +09:00
chanii8 9816a9c46a [as] feat: 첨부파일 다운로드 기능 추가 2026-06-29 13:49:33 +09:00
kiminchan f4b8f9d65d 우측 상세 패널 탭 지도 선택 동기화 2026-06-29 13:31:31 +09:00
mind-one005 08e8d7343c Merge commit '5505791b6e3ef04480731b123512e90e9e035c43' 2026-06-29 12:56:12 +09:00
mind-one005 5505791b6e wr: 하수재이용등록 화면 속도개선-오류수정 2026-06-29 11:48:37 +09:00
mind-one005 1862477641 wr: 하수재이용등록 화면 속도개선 2026-06-26 17:34:54 +09:00
kiminchan ff9daa2bfd Merge remote-tracking branch 'origin/iipbs/wr/dev' into iipbs/sn/dev_kic 2026-06-26 17:30:06 +09:00
kiminchan 23b13e3fdc 하수처리시설 레이어 기준 검색 동기화 2026-06-26 17:29:16 +09:00
mind-one005 f93f2674df sn: 지도 component 수정 반영 2026-06-26 17:23:03 +09:00
chanii8 1d7d34b7c1 Merge branch 'iipbs/as/dev'
Deploy Frontend / deploy (push) Successful in 2m16s
2026-06-26 16:12:39 +09:00
chanii8 9837a7af71 [as] refactor: 버튼 로직 중복 코드 제거 2026-06-26 16:12:18 +09:00
chanii8 d3ef4094c6 [as] feat: 설치인가 문서 삭제 버튼 기능 2026-06-26 15:57:22 +09:00
chanii8 bf86291016 [as] feat: 설치인가 버튼 로직 작업 2026-06-26 15:48:07 +09:00
kiminchan 37ace8b34c Merge remote-tracking branch 'origin/iipbs/wr/dev' into iipbs/sn/dev_kic 2026-06-26 15:46:35 +09:00
kiminchan e170790e4c 지반침하 레이어 설정 UI 및 포인트 표시 개선 2026-06-26 15:40:21 +09:00
kiminchan 0fe1aa983c 지반침하 반경 내 관로정보 요약 표시 개선 2026-06-26 10:35:40 +09:00
kiminchan 180eb1f59d 관로 이동 도구 on/off 유지 및 입력정보 조회 오류 방어 2026-06-26 10:15:24 +09:00
kiminchan 9ca61dd60d 지반침하 반경 내 관로정보 경과년도 집계 표시 2026-06-26 09:36:00 +09:00
chanii8 52a0635810 Merge remote-tracking branch 'origin/iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 2m53s
2026-06-26 09:15:38 +09:00
chanii8 2337b4cce2 Merge branch 'iipbs/as/dev' 2026-06-26 09:14:22 +09:00
chanii8 6f51b90178 [as] WIP: 설치인가 버튼 로직
[as] WIP: 설치인가 버튼 로직

[as] WIP: 설치인가 버튼 로직
2026-06-26 09:11:32 +09:00
rsone001 162e18142c [st] - 입력단 작업 완료 2026-06-25 18:02:31 +09:00
kiminchan d4797617a6 정보검색 팝업 초기 코드 로딩 및 최초 조회 보완 2026-06-25 10:40:28 +09:00
kiminchan be67395f13 정보검색 팝업 조회조건 공통코드 초기화 2026-06-25 10:22:40 +09:00
chanii8 20e17861e1 Merge branch 'iipbs/as/dev'
Deploy Frontend / deploy (push) Successful in 4m4s
2026-06-24 17:59:35 +09:00
chanii8 deb7375d6b [as] WIP: 설치인가 버튼 로직 2026-06-24 17:59:01 +09:00
kiminchan 2767260505 지반침하 레이어 아이콘 PNG 재인코딩 2026-06-24 16:13:52 +09:00
kiminchan bba4a8eeba 시설물 레이어 패널 토글 복구 2026-06-24 15:39:06 +09:00
kiminchan c9786adc77 Merge remote-tracking branch 'origin/main' into iipbs/sn/dev_kic 2026-06-24 14:53:40 +09:00
jcchoi 407051287d Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-06-24 09:59:53 +09:00
jcchoi 3d178dd6bc 사용자 검색 수정 2026-06-24 09:59:44 +09:00
chanii8 8ddedc60ac Merge branch 'iipbs/as/dev'
Deploy Frontend / deploy (push) Successful in 1m56s
2026-06-24 09:09:33 +09:00
chanii8 ddb8031e55 [as] WIP: 설치인가 페이지 2026-06-24 09:07:47 +09:00
rsone001 46cf24c193 [st] - 입력단 기능 작업중, 집계 요약표 다운로드 2026-06-23 10:20:20 +09:00
yangsein 82ab1376cc feat:행정동 탭코드 추가 2026-06-18 15:38:10 +09:00
kiminchan 2c27a67cc3 위성지도 기능 개선 및 침수 레이어 범례 보완 2026-06-18 11:03:39 +09:00
yangsein 5ea68435ed Merge remote-tracking branch 'origin/iipbs/st/dev' into feature/sein 2026-06-16 16:44:28 +09:00
yangsein 58ca85ce90 feat: 통계인구관리 내국인/외국인/행정동갱신 탭 구현" 2026-06-16 16:43:06 +09:00
kiminchan 2ea207a63a 통수위험 팝업 디자인 보완 2026-06-15 17:34:24 +09:00
kiminchan e47c00f0b6 침수흔적도 목록/지도 상세 팝업 mock 구현 및 md 업데이트 2026-06-15 16:28:37 +09:00
yangsein 7483d351c3 Merge remote-tracking branch 'origin/iipbs/st/dev' into feature/sein 2026-06-15 14:53:01 +09:00
kiminchan 83a3810301 시설물 레이어 ui 변경 작업 2026-06-12 13:26:35 +09:00
kiminchan c6309bf14d 지반침하정보 총 결과 건수 표시 2026-06-11 11:36:13 +09:00
kiminchan c10a2ae1c8 지반침하정보 아이콘 표출 오류 수정 2026-06-10 18:18:53 +09:00
kiminchan 3dd09d9fc7 하수관로 상세정보 조회속도 개선 2026-06-10 17:53:48 +09:00
kiminchan 46f05dbfa0 지도 최소 줌레벨 및 경계 개선 2026-06-10 16:12:18 +09:00
kiminchan 823d1d7812 지반침하 연도 조건과 팝업 조회조건 동기화 정리 2026-06-09 16:42:15 +09:00
kiminchan a274b5a261 지반침하 팝업 조회조건, 다운로드, 지도표출 방식 정리 2026-06-09 15:26:09 +09:00
kiminchan 8737d1eb0a gis 편집 스냅핑, 꼭지점 제거 기능 구현 2026-06-08 17:40:10 +09:00
kiminchan a123588e32 지반침하정보 지도, 상세, 설정, 목록 기능 구현 2026-06-05 16:30:58 +09:00
kiminchan cc91225b01 관로 분석 재질별, 경과년도&재질별 조회 오류 해결 2026-06-04 15:54:32 +09:00
kiminchan af8db9457f 노후관로 팝업 위치 수정 2026-06-04 15:16:53 +09:00
kiminchan 5860cf3a22 Merge remote-tracking branch 'origin/main' into iipbs/sn/dev_kic 2026-06-04 13:08:00 +09:00
kiminchan 9d6cc405a9 하수관망도 - 처리구역분석 상세정보 매설년도-> 경과년도 변경 2026-06-02 16:29:07 +09:00
kiminchan 1c37591bab 노후관로 경과년도 표시 조건 보완 2026-06-02 15:44:36 +09:00
mind-one001 c7c69ea5a7 gis 편집 2026-06-01 15:49:50 +09:00
mind-one001 cdaaa34569 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-05-21 15:57:02 +09:00
mind-one001 17637146be 유하방향 줌레벨 표시 추가 2026-05-21 15:56:33 +09:00
kiminchan b4033071a1 하수관로 편집 상세정보 편집 추가, 하수관망도 업로드 시설 선택 보완 2026-05-21 13:57:16 +09:00
kiminchan 0c66a8f3b7 통수위험분석 프로토타입 구현 2026-05-19 13:39:25 +09:00
kiminchan ef7bae98d4 하수관망도 업로드 구현 2026-05-18 10:23:33 +09:00
kiminchan d2a1f5e48b 정보검색, 관로분석(관재질) 팝업 테이블 가로 스크롤 추가 2026-05-15 15:34:25 +09:00
kiminchan 69b717f517 Merge remote-tracking branch 'origin/iipbs/dm/dev' into iipbs/sn/dev_kic 2026-05-15 15:06:46 +09:00
kiminchan 6d0461eaef 하수관로 편집 기능 구현 2026-05-15 15:03:00 +09:00
mind-one001 be72a594d7 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev
# Conflicts:
#	apps/sn/app/(map-layout)/map/page.tsx
2026-05-13 17:28:21 +09:00
mind-one001 8cc4f58076 Merge branch 'iipbs/sn/dev2' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev2 2026-05-13 17:17:38 +09:00
mind-one001 3f07739756 유하방향 기능 구현 2026-05-13 16:33:34 +09:00
1183 changed files with 129286 additions and 40109 deletions
+1
View File
@@ -29,4 +29,5 @@ settings.json
.agent
apps/**/tsconfig.tsbuildinfo
packages/**/tsconfig.tsbuildinfo
logs/
@@ -1 +0,0 @@
export { AcntgCodeManagementPage as default } from "@/pages/acntg-code-management";
@@ -1 +0,0 @@
export { CnstpCodeManagementPage as default } from "@/pages/cnstp-code-management";
@@ -0,0 +1 @@
export { CodeManagementPage as default } from "@/pages/code-management";
@@ -0,0 +1,10 @@
import { FnrsEditPage } from "@/pages/fnrs-cnsltn";
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function Page({ params }: PageProps) {
const { id } = await params;
return <FnrsEditPage id={id} />;
}
@@ -0,0 +1,10 @@
import { FnrsViewPage } from "@/pages/fnrs-cnsltn";
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function Page({ params }: PageProps) {
const { id } = await params;
return <FnrsViewPage id={id} />;
}
@@ -0,0 +1,3 @@
import { FnrsListPage } from "@/pages/fnrs-cnsltn";
export default FnrsListPage;
@@ -0,0 +1,12 @@
import { FnrsRegistPage } from "@/pages/fnrs-cnsltn";
import { parseTaskSeComCdFromApi } from "@/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema";
interface PageProps {
searchParams: Promise<{ taskSeComCd?: string }>;
}
export default async function Page({ searchParams }: PageProps) {
const params = await searchParams;
const taskSeComCd = parseTaskSeComCdFromApi(params.taskSeComCd);
return <FnrsRegistPage taskSeComCd={taskSeComCd.length ? taskSeComCd : undefined} />;
}
@@ -0,0 +1,10 @@
import { InstlEditPage } from "@/pages/instl-sncn";
interface InstlSncnEditRouteProps {
params: Promise<{ id: string }>;
}
export default async function Page({ params }: InstlSncnEditRouteProps) {
const { id } = await params;
return <InstlEditPage id={id} />;
}
@@ -1,10 +0,0 @@
import { CnfmModifyPage } from "@/pages/instl-sncn";
export default async function InstlSncnModifyPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return <CnfmModifyPage id={id} />;
}
@@ -1,10 +1,6 @@
import { CnfmViewPage } from "@/pages/instl-sncn";
export default async function InstlSncnViewPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return <CnfmViewPage id={id} />;
}
import { InstlViewPage } from "@/pages/instl-sncn";
export default async function InstlSncnDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
return <InstlViewPage id={id} />;
}
@@ -1,3 +1,3 @@
import { CnfmListPage } from "@/pages/instl-sncn";
export default CnfmListPage;
import { InstlListPage } from "@/pages/instl-sncn";
export default InstlListPage;
@@ -0,0 +1,14 @@
import { InstlRegistPage } from "@/pages/instl-sncn";
interface InstlSncnRegistRouteProps {
searchParams: Promise<{ taskSeComCd?: string }>;
}
export default async function Page({ searchParams }: InstlSncnRegistRouteProps) {
const params = await searchParams;
const taskSeComCd = params.taskSeComCd
? params.taskSeComCd.split(",").filter(Boolean)
: undefined;
return <InstlRegistPage taskSeComCd={taskSeComCd} />;
}
@@ -1,5 +0,0 @@
import { CnfmRegistPage } from "@/pages/instl-sncn";
export default function Page(props: any) {
return <CnfmRegistPage {...props} />;
}
+6 -23
View File
@@ -5,6 +5,7 @@ import {
ACCESS_TOKEN,
AUTH_USER_ID,
CLAIM_NAME,
COOKIE_DOMAIN,
REFRESH_TOKEN,
SERVER_API_URL,
SITE_ID,
@@ -20,6 +21,10 @@ async function handler(
"Content-Type": "application/json",
"X-Site-Id": SITE_ID,
};
const deviceId = request.headers.get("X-Device-Id");
if (deviceId) {
headers["X-Device-Id"] = deviceId;
}
let body = {};
const isRefreshHandler = params.url.includes("refresh");
@@ -39,27 +44,13 @@ async function handler(
}
const targetUrl = `${SERVER_API_URL}/${params.url.join("/")}`;
console.log(`[Proxy API] ${method} ${params.url.join("/")} -> ${targetUrl}`);
console.log(`[Proxy API] Headers:`, headers);
if (!isRefreshHandler) {
console.log(`[Proxy API] Body:`, body);
}
const result = await fetch(targetUrl, {
method,
headers,
body: JSON.stringify(body),
});
console.log(`[Proxy API] Response Status: ${result.status}`);
console.log(`[Proxy API] Response Headers:`, {
[ACCESS_TOKEN]: result.headers.get(ACCESS_TOKEN),
[REFRESH_TOKEN]: result.headers.get(REFRESH_TOKEN) ? '***hidden***' : undefined,
[AUTH_USER_ID]: result.headers.get(AUTH_USER_ID),
});
if (!result.ok) {
console.error(`[Proxy API] Request failed with status ${result.status}`);
return NextResponse.json({ message: "Invalid Credentials" }, { status: 401 });
}
@@ -68,24 +59,16 @@ async function handler(
const userId = result.headers.get(AUTH_USER_ID);
if (accessToken && userId) {
console.log(`[Proxy API] Login successful for user: ${userId}`);
const response = NextResponse.json({
[ACCESS_TOKEN]: accessToken,
[AUTH_USER_ID]: userId,
});
const forwardedHost = request.headers.get("x-forwarded-host");
const host = request.headers.get("host");
const actualHost = forwardedHost || host || new URL(request.url).hostname.toString();
// 운영 환경에서 domain 설정
const domain = actualHost.includes("hasuon.or.kr") ? ".hasuon.or.kr" : actualHost;
if (refreshToken) {
cookieStore.set(REFRESH_TOKEN, refreshToken, {
httpOnly: true,
sameSite: "strict",
domain: domain,
domain: COOKIE_DOMAIN,
path: "/",
});
}
@@ -0,0 +1,367 @@
# 재원협의(fnrs-cnsltn) vs 설치인가(instl-sncn) 차이 분석
| 항목 | 내용 |
| --------- | ----------------------------------------------------------------------------- |
| 작성일 | 2026-07-09 |
| 기준 구현 | `fnrs-cnsltn` (추가 요구사항 반영 완료) |
| 비교 대상 | `instl-sncn` |
| 목적 | 두 페이지의 동작·구조 차이를 정리하고, 설치인가 정렬 시 이식 대상을 명확히 함 |
---
## 1. 요약
두 모듈은 **동일한 PBADMS(공공행정 지원) 업무 패턴**을 따르는 형제 페이지다.
공통 흐름:
```
목록 → TaskSe 선택 후 신청 → 저장(SV) → 신청완료(ST)
→ 환경청제출(LE) → 기술검토 → 검토결과 회신(EP)
→ 인가승인(TA) → 고시(TB)
```
| 구분 | fnrs-cnsltn | instl-sncn |
| ------------- | ----------------- | ---------------- |
| 업무명 | 재원협의 | 설치인가 |
| 문서번호 | `fnrsCnsltnDocNo` | `instlSncnDocNo` |
| docSeComCd | `FNRS` | `INSTL` |
| 라우트 | `/fnrs-cnsltn/*` | `/instl-sncn/*` |
| pages 파일 수 | 39 | 32 |
**결론:** `fnrs-cnsltn`이 추가 요구사항이 반영된 기준(reference) 구현이고, `instl-sncn`은 동일 골격이지만 보완요청·결재선 편집·등록/수정 폼 완성도 등에서 미반영·TODO·단순화 상태다.
---
## 2. 파일/폴더 구조
### 2-1. fnrs-cnsltn에만 존재
| 경로 | 역할 |
| -------------------------------------------- | ------------------------- |
| `ui/sections/fnrs-shar-section.tsx` | 재원분담 조회 섹션 |
| `ui/sections/supplement-demand-section.tsx` | 보완요청 이력 섹션 |
| `ui/components/sortable-prgrs-stts-row.tsx` | 결재선 DnD 편집 행 |
| `ui/components/sortable-apln-picker-row.tsx` | 결재선 템플릿 피커 DnD 행 |
| `ui/components/aprvr-picker-dialog.tsx` | 결재자 선택 다이얼로그 |
| `ui/components/elapr-apln-picker-grid.tsx` | 결재선 편집 그리드 |
| `ui/components/elapr-tmplt-select-table.tsx` | 결재서식 선택 테이블 |
| `model/reviewer-option.ts` | 결재자 옵션 타입 |
### 2-2. instl-sncn에만 존재
| 경로 | 역할 | 비고 |
| -------------------------------------- | ------------------- | ------------------------------------------ |
| `ui/sections/history-info-section.tsx` | 등록/수정 이력 표시 | **어디에서도 import되지 않음 (dead code)** |
### 2-3. 공통 구조
```
index.ts
ui/
{list,view,form,regist,edit}-page.tsx
sections/ (basic-info, contact, scale, cost, checklist, attachments, tech-review)
components/ (floating-action-buttons, elapr-approval-dialog, elapr-tmplt-picker-dialog, checklist-task-panel)
lib/ button-config, save-payload-utils, form-data-mapper, regist-schema, checklist/review utils
model/ columns.tsx, view-types.ts
```
App Router도 동일: `page.tsx`, `regist/page.tsx`, `[id]/page.tsx`, `[id]/edit/page.tsx`
---
## 3. 목록 페이지
### 3-1. 검색 필터
| 항목 | fnrs-cnsltn | instl-sncn |
| ------------------ | ----------------------------------------- | --------------------------------- |
| 시도/시군구 API | `@/entities/adstdstr` | 동일 |
| 지역 검색 파라미터 | **`admdstCd`** (시군구 우선, 없으면 시도) | **`sidoComCd` + `sggComCd`** 분리 |
| "전체" 옵션 | `ALL_SELECT_VALUE = "__ALL__"` SelectItem | 없음 (placeholder만) |
| 구분 CodeSelect | `upCd="INSTL"` ※ | `upCd="INSTL"` |
| 사업명 | 동일 | 동일 |
> ※ fnrs 목록의 구분 필터가 `INSTL` 코드를 사용 중이며, TaskSePickerDialog는 `codeUpCd="FNRS"`를 쓴다. **불일치 가능성**이 있어 확인이 필요하다.
### 3-2. 정렬·페이지네이션
| 항목 | fnrs-cnsltn | instl-sncn |
| ------------------------ | ---------------------- | ------------------------------------- |
| 기본 sort (list-page) | `fnrsCnsltnDocNo,desc` | `instlSncnDocNo,desc` |
| fallback sort (list.tsx) | `fnrsCnsltnDocNo,desc` | **`regDt,desc`** (list-page와 불일치) |
| 페이지 크기 | 10 | 10 |
| URL 동기화 | 없음 (`useState` 로컬) | 없음 |
### 3-3. 테이블 컬럼
`model/columns.tsx` 로직은 사실상 동일하다. DTO 타입명만 다르다.
컬럼: 시도, 시군구, 사업명, 업무구분, 담당자, 해당기관, 진행상황, 신청일
### 3-4. 행 클릭·신청
| 항목 | fnrs-cnsltn | instl-sncn |
| ------------ | ------------------------------------------------------------------ | ------------------ |
| 행 클릭 | **조건부 라우팅**: `SV`/`SL` + 등록자 본인 → `/edit`, 그 외 → view | **항상 view** |
| useUserStore | 사용 | 미사용 |
| 신청 버튼 | TaskSePickerDialog `codeUpCd="FNRS"` | `codeUpCd="INSTL"` |
```46:54:apps/as/src/pages/fnrs-cnsltn/ui/fnrs-list.tsx
const handleRowClick = (row: FnrsCnsltnMngResponseDto) => {
const canEdit =
(row.prgrsSttsComCd === "SV" || row.prgrsSttsComCd === "SL") &&
row.rgtr === userInfo?.userUnqId;
router.push(
canEdit ? `/fnrs-cnsltn/${row.fnrsCnsltnDocNo}/edit` : `/fnrs-cnsltn/${row.fnrsCnsltnDocNo}`,
);
};
```
---
## 4. 상세(View) 페이지
### 4-1. 섹션 구성
| 섹션 | fnrs-cnsltn | instl-sncn |
| ------------------------------- | ------------------------------------------ | -------------------------- |
| BasicInfo | 환경청구분, 확정예산사업번호, 예산신청연도 | **사업구분(`sncnSeCdNm`)** |
| ContactInfo | 동일 패턴 | 동일 |
| BusinessScale / Cost | 동일 | 동일 |
| **FnrsShar (재원분담)** | **있음** | **없음** (도메인 고유) |
| Checklist | 동일 | 동일 |
| Attachments | 동일 | 동일 |
| **SupplementDemand (보완요청)** | **있음** | **없음** |
| TechReviewOpinion | 동일 | 동일 |
| HistoryInfo | 없음 | 파일만 존재, view 미연결 |
| FloatingActionButtons | `docSeComCd="FNRS"` | `docSeComCd="INSTL"` |
### 4-2. 데이터 훅
| 항목 | fnrs-cnsltn | instl-sncn |
| --------- | ---------------------------- | -------------------- |
| 상세 훅 | `useFnrsCnsltnMng` | `useInstlSncnDetail` |
| 에러 판정 | `isError \|\| !data?.detDto` | `isError \|\| !data` |
| 로딩 | Skeleton | Skeleton |
---
## 5. 등록/수정(Form) 페이지
### 5-1. 폼 완성도
| 항목 | fnrs-cnsltn | instl-sncn |
| ------------- | --------------------------------- | --------------- |
| 저장(SV) | O | O (SV 고정) |
| 신청완료(ST) | O (edit 모드) | **없음** |
| 삭제 | O (edit 모드, `useConfirm`) | **없음** |
| 재원분담 섹션 | `FnrsSharFormSection` + 잔액 검증 | 없음 |
| 저장 후 이동 | **`/edit` 유지** | **view로 이동** |
| 목록 버튼 | `/fnrs-cnsltn` 고정 | `router.back()` |
### 5-2. 기본정보 Form 필드
| 필드 | fnrs | instl |
| ----------------- | ---------------- | --------------------------------------- |
| taskSe 코드 | `FNRS` | `INSTL` |
| 사업구분 | 없음 | **`sncnSeComCd` (SNCNSE 라디오, 필수)** |
| BnsPicker 연동 | bizNm ~ bizCn | + **`bgtBizCd`, `bgtAplyYr`** |
| 재이용(RE) 조건부 | rtlztnFclt / Usg | 동일 |
### 5-3. Schema
| 항목 | fnrs | instl |
| ------------------- | ------------------- | -------------- |
| 관리 객체 | `fnrsCnsltnMng` | `instlSncnMng` |
| 예산 필드 | `cfmtnBgtBizNo` | `bgtBizCd` |
| 재원분담 | `fnrsSharList` 배열 | 없음 |
| sharAtchFileGroupId | 있음 | 없음 |
| privacyConsent | 있음 | 있음 |
### 5-4. Edit 페이지 래퍼
| 항목 | fnrs | instl |
| ------------------ | ------------------------------- | ------------------------------- |
| 로딩 Skeleton | 있음 | **없음** |
| 데이터 없음 메시지 | "상세 정보를 찾을 수 없습니다." | "수정 대상을 찾을 수 없습니다." |
---
## 6. FloatingActionButtons / 결재 / 보완
### 6-1. FAB 동작 차이
공통: 역할×상태 기반 버튼(`button-config.ts`), checklist 진행률 검증, elapr pending approval 체크
| 기능 | fnrs-cnsltn | instl-sncn |
| ------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- |
| 보완완료 (`splmntCmptn`) | **`submitSupplement` API 연동** | **`// TODO` stub** |
| 보완요청 (`dmndSplmnt`) | **`buildSupplementReqPayload` + `submitReqSupplement`** | **`// TODO` stub** (문자열만 전달) |
| 검토완료 (`rvwCmptn`) | `techReViewSaveCd: "SV"` 후 **결재 다이얼로그 오픈** | `techReViewSaveCd: "CP"`, **다이얼로그 없이 즉시 완료** |
| 검토자지정 | `buildElaprPrgrsSttsListFromAplnRows(aplnRows)` | `buildElaprPrgrsSttsListFromApln(aplnPage.content)` |
| atrzAprv / atrzRply / grndsChck | TODO handler | TODO handler |
```576:617:apps/as/src/pages/instl-sncn/ui/components/floating-action-buttons.tsx
async function handleSplmntCmptn(context: ActionHandlerContext): Promise<void> {
// TODO: 보완완료 API 호출
context.onSuccess?.();
}
// ...
async function handleDmndSplmnt(context: ActionHandlerContext): Promise<void> {
// TODO: 보완요청 API 호출
context.onSuccess?.();
}
```
### 6-2. ElaprApprovalDialog (가장 큰 UX 격차)
| 항목 | fnrs (419줄) | instl (231줄) |
| ---------------- | ------------------------------------------------------ | -------------------------------- |
| 결재선 표시 | DnD sortable 편집 테이블 | **읽기 전용** 테이블 |
| 결재선 추가/저장 | **지원** (`usePrgrsSttsRows`, `useSaveElaprPrgrsStts`) | 없음 |
| 결재자 변경 | **AprvrPickerDialog** | 없음 |
| 전결 체크박스 | **있음** (`finalApprovalYn`) | 없음 |
| 반려 사유 | API에 `rjctRsn` 전달 | UI는 있으나 **payload에 미전달** |
| 저장 전 검증 | `hasUnsavedPrgrsSttsChanges` | 없음 |
### 6-3. ElaprTmpltPickerDialog
| 항목 | fnrs | instl |
| --------------- | -------------------------------------------------------------------- | ----------------------- |
| UI | 2-pane: 서식 테이블 + **결재선 편집 그리드** | 단일 테이블 + 선택 버튼 |
| docSeComCd 필터 | `FNRS` | `INSTL` |
| onConfirm | `{ tmplt, aplnRows }` 편집 결과 | `onSelect(tmplt)`만 |
| 검증 | `validateAllRowsForSave`, `validateAtrzOrder`, `validateUniqueSeqNo` | 없음 |
---
## 7. Entity / API / Hook
### 7-1. Service 메서드
| 메서드 | fnrs | instl |
| ----------------------- | --------------------------- | --------- |
| 목록 | `findPage` | `getPage` |
| 상세 | `findById` | `getById` |
| saveApply | O | O |
| techReview | O | O |
| changePrgrsSttsComCd | O | O |
| remove | O | O |
| **submitSupplement** | **O** (`/submitsupplement`) | **X** |
| **submitReqSupplement** | **O** (`/reqsupplement`) | **X** |
### 7-2. 상세 DTO 확장
**fnrs 추가 필드**
- `fnrsSharList`, `sharAtchFileList`
- `splmntDmndList` (typed)
**instl 추가 필드**
- `sncnSeComCd` / `sncnSeCdNm` (인가구분)
- `bgtBizCd` (예산사업코드)
- `rtlztnFcltSeComNm`, `rtlztnUsgSeComNm`
- `splmntDmndList` 타입은 base에 존재하나 view/API 미연결
- `instlSncnRegionService` (시도/시군구 — 목록은 adstdstr 사용, 미활용)
### 7-3. save-payload-utils
| 함수 | fnrs | instl |
| -------------------------------- | ---------------- | --------- |
| buildSavePayload | + `fnrsSharList` | shar 없음 |
| buildTechReviewReplyPayload | O | O |
| **buildSupplementReqPayload** | **O** | **X** |
| **buildSupplementSubmitPayload** | **O** | **X** |
---
## 8. 비즈니스 도메인 차이
| 영역 | fnrs-cnsltn | instl-sncn |
| -------------- | ----------------------------------------- | ------------------------ |
| 업무 코드 | `FNRS` | `INSTL` |
| TaskSe | FNRS 하위 | INSTL 하위 |
| 고시 버튼 라벨 | "재원협의 고시" | "설치인가 고시" |
| 재원분담 | **핵심 도메인** (`fnrs-shar-se`) | 해당 없음 |
| 인가구분 | 없음 | **`SNCNSE` 코드 (필수)** |
| 예산 식별 | `cfmtnBgtBizNo` | `bgtBizCd` |
| 진행상태 코드 | SV, ST, LE, ER, TE, EP, TA, TB, SL, TP 등 | **동일 세트 공유** |
| 역할 매핑 | ROLE_USER05/04/03/01 | 동일 |
---
## 9. fnrs-cnsltn에만 있는 기능 (추가 요구사항 후보)
instl-sncn 정렬 시 **우선 이식 대상**:
1. 목록 행 클릭 조건부 edit 라우팅 (`SV`/`SL` + 등록자)
2. 보완요청 전체 플로우 — view 섹션, payload builder, API, FAB handler
3. 보완완료 API 연동 (`submitSupplement`)
4. 등록/수정 폼 완성 — 신청완료, 삭제, 저장 후 edit 유지
5. 결재선 DnD 편집형 `ElaprApprovalDialog` (+ aprvr-picker, sortable rows)
6. 결재선 편집형 `ElaprTmpltPickerDialog` (+ apln grid, validation)
7. 검토완료 → 결재 다이얼로그 연계 흐름
8. 검색 필터 "전체" 옵션 및 `admdstCd` 통합 방식 (백엔드 API에 맞게)
9. Edit 페이지 로딩 Skeleton
10. entity에 supplement API/hooks 추가
---
## 10. instl-sncn에만 있는 기능 (유지 필요)
1. **`sncnSeComCd` (사업구분/인가구분)** — 도메인 고유
2. **`bgtBizCd` / `bgtAplyYr`** BnsPicker 연동
3. **`history-info-section.tsx`** — 구현만 있고 view 미연결 (활성화 또는 삭제 결정 필요)
4. entity **`instlSncnRegionService`** — 목록은 adstdstr 사용 중이라 중복/레거시 가능
5. **`INSTL_SNCN_REGISTER_SELECTION_KEY`** sessionStorage 키
---
## 11. 동일·거의 동일 패턴
| 영역 | 동일도 |
| ----------------------------------------------------- | --------------------- |
| App Router 4-route 구조 | 100% |
| `button-config.ts` 역할×상태 매트릭스 | ~99% (액션명만 다름) |
| `columns.tsx` 진행상황 Badge | ~100% |
| checklist / contact / scale / cost / attachments 섹션 | ~95% (import 경로만) |
| review-step-utils 로직 | ~100% (상수명만 다름) |
| TaskSePickerDialog 신청 플로우 | 100% |
| FAB 전체 구조·대부분 handler | ~85% |
| checklist-form-state, business-cost-utils | 거의 동일 |
| shared widgets (`@/widgets/pbadms-sprt`) | 동일 |
---
## 12. instl-sncn 정렬 권장 우선순위
| 우선순위 | 항목 | 근거 |
| -------- | ------------------------------------------------------- | ---------------------------- |
| P0 | 보완요청/보완완료 API + UI | fnrs 완전 구현 vs instl TODO |
| P0 | ElaprApprovalDialog 고도화 | 결재선 편집·전결·반려사유 |
| P0 | ElaprTmpltPickerDialog 2-pane | 검토자지정 품질 |
| P1 | Form 신청완료/삭제 + 저장 후 edit 유지 | 등록 UX parity |
| P1 | 목록 행 클릭 edit 라우팅 | fnrs 추가 요구사항 |
| P1 | 검토완료 → 결재 다이얼로그 연계 | 흐름 통일 |
| P2 | sort fallback 불일치 수정 (`regDt` vs `instlSncnDocNo`) | instl 내부 버그성 |
| P2 | history-info-section 연결 또는 삭제 | dead code 정리 |
| P3 | URL search params 동기화 | 양쪽 모두 미구현 (공통 개선) |
---
## 13. 참고 코드 위치
| 주제 | 경로 |
| ---------------------------- | ------------------------------------------------------------ |
| 목록 edit 라우팅 (fnrs) | `pages/fnrs-cnsltn/ui/fnrs-list.tsx` |
| 목록 항상 view (instl) | `pages/instl-sncn/ui/instl-list.tsx` |
| view 추가 섹션 (fnrs) | `pages/fnrs-cnsltn/ui/fnrs-view-page.tsx` |
| FAB 보완 TODO (instl) | `pages/instl-sncn/ui/components/floating-action-buttons.tsx` |
| 결재 다이얼로그 (fnrs) | `pages/fnrs-cnsltn/ui/components/elapr-approval-dialog.tsx` |
| 결재 다이얼로그 (instl) | `pages/instl-sncn/ui/components/elapr-approval-dialog.tsx` |
| 폼 완성도 (fnrs) | `pages/fnrs-cnsltn/ui/fnrs-form-page.tsx` |
| 폼 단순화 (instl) | `pages/instl-sncn/ui/instl-form-page.tsx` |
| supplement API (fnrs entity) | `entities/fnrs-cnsltn/api/fnrs-cnsltn-service.ts` |
+1
View File
@@ -1,6 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
reactStrictMode: true,
transpilePackages: ["@common/assets"],
};
+3
View File
@@ -20,16 +20,19 @@
"@faker-js/faker": "^9.9.0",
"@frontend-opensource/use-react-hooks": "^1.1.4",
"@hookform/resolvers": "^4.1.3",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^5.75.5",
"@tanstack/react-query-devtools": "^5.75.5",
"@tanstack/react-table": "^8.21.2",
"axios": "^1.8.1",
"class-variance-authority": "^0.7.1",
"dayjs": "^1.11.13",
"js-cookie": "^3.0.5",
"lucide-react": "^0.474.0",
"next": "15.5.18",
"react-hook-form": "^7.54.2",
"tus-js-client": "^4.3.1",
"uuid": "^14.0.1",
"zod": "^3.24.2"
},
"devDependencies": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+2
View File
@@ -7,6 +7,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { useMenuStore } from "@/shared/lib";
import { PageLayout } from "@/shared/ui/page-layout";
import { authService } from "@/entities/auth";
import type { MenuItem } from "@/entities/menu";
import { BaseLayout } from "@common/components";
@@ -32,6 +33,7 @@ export function BaseLayoutWrapper({ children }: BaseLayoutWrapperProps) {
return (
<BaseLayout
queryClient={queryClient}
authService={authService}
currentPath={pathname ?? ""}
handleMenuClick={handleMenuClick}
>
+3 -1
View File
@@ -27,7 +27,9 @@ export function Providers({ children }: ProvidersProps) {
</AuthWrapper>
</ConfirmProvider>
</StoreProvider>
<ReactQueryDevtools initialIsOpen={false} />
<div style={{ fontSize: "1.4rem" }}>
<ReactQueryDevtools initialIsOpen={false} />
</div>
</QueryClientProvider>
);
}
@@ -12,7 +12,7 @@ import {
type AcntgSeMngTreeDto,
} from "../model/acntg-interface";
const BASE = ENDPOINT.AS_COMMON_SERVICE.ACNTG_SE;
const BASE = ENDPOINT.AS_SERVICE.ACNTG_SE;
async function findAll(params: AcntgSeMngSearchDto): Promise<PageResponse<AcntgSeMngResponseDto>> {
const response = await client.get<PageResponse<AcntgSeMngResponseDto>>(BASE, { params });
+1 -1
View File
@@ -10,7 +10,7 @@ import type {
import { acntgSeMngService } from "./acntg-service";
const acntgSeMngBaseKey = [ENDPOINT.AS_COMMON_SERVICE.ACNTG_SE] as const;
const acntgSeMngBaseKey = [ENDPOINT.AS_SERVICE.ACNTG_SE] as const;
export function useAcntgSeMngPage(params: AcntgSeMngSearchDto) {
return useQuery({
@@ -16,6 +16,7 @@ export interface AcntgSeMngResponseDto {
cdLvNo: number;
useYn: string;
expln?: string;
sortSeq?: number;
rgtr?: string;
rgtrNm?: string;
mdfr?: string;
@@ -39,6 +40,7 @@ export interface AcntgSeMngSaveRequestDto {
cdLvNo: number;
useYn?: string;
expln?: string;
sortSeq?: number;
}
/** PUT /{acntgSeCd} 요청 (mode는 서비스에서 U로 설정) */
@@ -0,0 +1,26 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
CmCdAdstDstrComboResponseDto,
CmCdAdstDstrService,
} from "../model/adstdstr-interface";
const BASE = ENDPOINT.AS_SERVICE.ADSTDSTR;
async function findSidoCombo(): Promise<CmCdAdstDstrComboResponseDto[]> {
const response = await client.get<CmCdAdstDstrComboResponseDto[]>(`${BASE}/sido`);
return response.data;
}
async function findGugunCombo(sido: string): Promise<CmCdAdstDstrComboResponseDto[]> {
const response = await client.get<CmCdAdstDstrComboResponseDto[]>(`${BASE}/gugun`, {
params: { sido },
});
return response.data;
}
export const cmCdAdstDstrService = {
findSidoCombo,
findGugunCombo,
} satisfies CmCdAdstDstrService;
@@ -0,0 +1,22 @@
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import { cmCdAdstDstrService } from "./adstdstr-service";
const cmCdAdstDstrBaseKey = [ENDPOINT.AS_SERVICE.ADSTDSTR] as const;
export function useFindSidoCombo() {
return useQuery({
queryKey: [...cmCdAdstDstrBaseKey, "findSidoCombo"],
queryFn: () => cmCdAdstDstrService.findSidoCombo(),
});
}
export function useFindGugunCombo(sido: string) {
return useQuery({
queryKey: [...cmCdAdstDstrBaseKey, "findGugunCombo", sido],
queryFn: () => cmCdAdstDstrService.findGugunCombo(sido),
enabled: Boolean(sido),
});
}
+6
View File
@@ -0,0 +1,6 @@
export { cmCdAdstDstrService } from "./api/adstdstr-service";
export { useFindGugunCombo, useFindSidoCombo } from "./api/queries";
export type {
CmCdAdstDstrComboResponseDto,
CmCdAdstDstrService,
} from "./model/adstdstr-interface";
@@ -0,0 +1,11 @@
/** 백엔드 CmCdAdstDstrComboResponseDto */
export interface CmCdAdstDstrComboResponseDto {
adstDstrCd: string;
adstDstrNm: string;
}
/** Controller public 메서드명·시그니처와 동일 */
export interface CmCdAdstDstrService {
findSidoCombo(): Promise<CmCdAdstDstrComboResponseDto[]>;
findGugunCombo(sido: string): Promise<CmCdAdstDstrComboResponseDto[]>;
}
@@ -0,0 +1,22 @@
import { AxiosResponse } from "axios";
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { AttachService } from "../model/attach-interface";
async function deleteAttachmentByAtflId(atflId: string): Promise<void> {
if (!atflId) return;
await client.delete(`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflId}`);
}
async function downloadAttachmentByAtflCd(atflCd: string): Promise<AxiosResponse> {
return await client.get<Blob>(`${ENDPOINT.ATTACH_SERVICE.DOWNLOAD}/${atflCd}`, {
responseType: "blob",
});
}
export const attachService = {
deleteAttachmentByAtflId,
downloadAttachmentByAtflCd,
} satisfies AttachService;
@@ -0,0 +1,39 @@
import { useMutation } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import { parseFilenameFromResponse } from "../lib/parse-filename-from-response";
import { attachService } from "./attach-service";
const attachBaseKey = [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS] as const;
const attachDownloadBaseKey = [ENDPOINT.ATTACH_SERVICE.DOWNLOAD] as const;
export function useDeleteAttachment() {
return useMutation({
mutationKey: [...attachBaseKey, "delete"],
mutationFn: (atflId: string) => attachService.deleteAttachmentByAtflId(atflId),
});
}
export function useDownloadAttachmentByAtflCd() {
return useMutation({
mutationKey: [...attachDownloadBaseKey, "download"],
mutationFn: async (atflCd: string) => {
const response = await attachService.downloadAttachmentByAtflCd(atflCd);
const filename = parseFilenameFromResponse(response);
const blob = new Blob([response.data]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(link);
},
});
}
+3
View File
@@ -0,0 +1,3 @@
export { attachService } from "./api/attach-service";
export { useDeleteAttachment, useDownloadAttachmentByAtflCd } from "./api/queries";
export type { Attach, AttachService } from "./model/attach-interface";
@@ -0,0 +1,11 @@
import type { AxiosResponse } from "axios";
export function parseFilenameFromResponse(response: AxiosResponse): string {
const contentDisposition = response.headers["content-disposition"];
if (!contentDisposition) return "download";
const filenameMatch = contentDisposition.match(/filename\*=UTF-8''([^;]+)/);
if (!filenameMatch?.[1]) return "download";
return decodeURIComponent(filenameMatch[1]);
}
@@ -0,0 +1,22 @@
import { AxiosResponse } from "axios";
/** 백엔드 AtflMngResponseDto */
export interface Attach {
atflCd: string;
atflSn: number;
atflId: string;
physFileNm: string;
orgnlFileNm: string;
atflFlieSz: number;
atflType: string;
dwnldNocs: number;
delYn: string;
lnkgDmnId: string;
lnkgDmnNm: string;
regDt: string;
}
export interface AttachService {
deleteAttachmentByAtflId: (atflId: string) => Promise<void>;
downloadAttachmentByAtflCd: (atflCd: string) => Promise<AxiosResponse>;
}
+15 -6
View File
@@ -1,13 +1,16 @@
import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config";
import { getRemainingMs } from "@common/auth";
import { type AuthPayload, type AuthService, getRemainingMs } from "@common/auth";
import { clearAuthToken, setToken, setupRefreshTimer } from "../lib/token-manager";
import type { AuthPayload, AuthService } from "../model/auth-interface";
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
import { clearRefreshTimer, setToken, setupRefreshTimer, tokenManager } from "../lib/token-manager";
async function login(payload: AuthPayload): Promise<void> {
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: {
"Content-Type": "application/json",
"X-Device-Id": getOrCreateDeviceId(),
},
body: JSON.stringify(payload),
});
@@ -34,6 +37,9 @@ async function silentRefresh(): Promise<void> {
try {
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`, {
method: "PUT",
headers: {
"X-Device-Id": getOrCreateDeviceId(),
},
});
const data = await response.json();
@@ -58,7 +64,7 @@ async function silentRefresh(): Promise<void> {
}
} catch (error) {
console.error(`[silentRefresh] Failed at ${new Date().toISOString()}`, error);
clearAuthToken();
tokenManager.clearAuthToken();
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("auth:logout"));
}
@@ -66,7 +72,9 @@ async function silentRefresh(): Promise<void> {
}
async function logout(): Promise<void> {
clearAuthToken();
tokenManager.clearAuthToken();
clearDeviceId();
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("auth:logout"));
}
@@ -75,5 +83,6 @@ async function logout(): Promise<void> {
export const authService: AuthService = {
login,
silentRefresh,
declineSessionExtend: clearRefreshTimer,
logout,
};
@@ -0,0 +1,17 @@
import { v4 as uuidv4 } from "uuid";
const DEVICE_ID_KEY = "x-device-id";
export function getOrCreateDeviceId(): string {
if (typeof window === "undefined") return "";
const existing = localStorage.getItem(DEVICE_ID_KEY);
if (existing) return existing;
const id = uuidv4();
localStorage.setItem(DEVICE_ID_KEY, id);
return id;
}
export function clearDeviceId(): void {
if (typeof window === "undefined") return;
localStorage.removeItem(DEVICE_ID_KEY);
}
@@ -8,7 +8,7 @@ export const {
setToken,
removeToken,
setupRefreshTimer,
clearAuthToken,
clearRefreshTimer,
getAccessToken,
getRefreshToken,
getUserId,
@@ -1,10 +0,0 @@
export interface AuthPayload {
userId: string;
password: string;
}
export interface AuthService {
login(payload: AuthPayload): Promise<void>;
silentRefresh(): Promise<void>;
logout(): Promise<void>;
}
@@ -0,0 +1,47 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import { buildBnsSearchDto } from "../lib/build-bns-search-dto";
import type {
BnsDetailResponseWrapperDto,
BnsIfService,
BnsListResponseWrapperDto,
BnsSearchDto,
EmCodeListResponseDto,
} from "../model/bns-if-interface";
const BASE = ENDPOINT.AS_SERVICE.BNS_IF;
async function getBnsList(params: BnsSearchDto): Promise<BnsListResponseWrapperDto> {
const response = await client.get<BnsListResponseWrapperDto>(`${BASE}/bns`, {
params: buildBnsSearchDto(params),
});
return response.data;
}
async function getBnsDetail(
gbn: string,
yr: string,
seqNo: string,
): Promise<BnsDetailResponseWrapperDto> {
const response = await client.get<BnsDetailResponseWrapperDto>(`${BASE}/bns/detail`, {
params: { gbn, yr, seqNo },
});
return response.data;
}
async function getEmCodeList(
dpmCd?: string,
lclgvrnmntCd?: string,
): Promise<EmCodeListResponseDto> {
const response = await client.get<EmCodeListResponseDto>(`${BASE}/emCodeList`, {
params: { dpmCd, lclgvrnmntCd },
});
return response.data;
}
export const bnsIfService = {
getBnsList,
getBnsDetail,
getEmCodeList,
} satisfies BnsIfService;
@@ -0,0 +1,33 @@
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import type { BnsSearchDto } from "../model/bns-if-interface";
import { bnsIfService } from "./bns-if-service";
const bnsIfBaseKey = [ENDPOINT.AS_SERVICE.BNS_IF] as const;
export function useBnsList(params: BnsSearchDto, options?: { enabled?: boolean }) {
return useQuery({
queryKey: [...bnsIfBaseKey, "getBnsList", params],
queryFn: () => bnsIfService.getBnsList(params),
enabled: options?.enabled,
placeholderData: keepPreviousData,
});
}
export function useBnsDetail(gbn: string, yr: string, seqNo: string) {
return useQuery({
queryKey: [...bnsIfBaseKey, "getBnsDetail", gbn, yr, seqNo],
queryFn: () => bnsIfService.getBnsDetail(gbn, yr, seqNo),
enabled: Boolean(gbn && yr && seqNo),
});
}
export function useEmCodeList(dpmCd?: string, lclgvrnmntCd?: string) {
return useQuery({
queryKey: [...bnsIfBaseKey, "getEmCodeList", dpmCd, lclgvrnmntCd],
queryFn: () => bnsIfService.getEmCodeList(dpmCd, lclgvrnmntCd),
});
}
+13
View File
@@ -0,0 +1,13 @@
export { bnsIfService } from "./api/bns-if-service";
export { useBnsDetail, useBnsList, useEmCodeList } from "./api/queries";
export { buildBnsSearchDto } from "./lib/build-bns-search-dto";
export type {
BnsDetailResponseDto,
BnsDetailResponseWrapperDto,
BnsIfService,
BnsListResponseDto,
BnsListResponseWrapperDto,
BnsSearchDto,
EmCodeItemDto,
EmCodeListResponseDto,
} from "./model/bns-if-interface";
@@ -0,0 +1,20 @@
import type { BnsSearchDto } from "../model/bns-if-interface";
export function buildBnsSearchDto(input: {
cPage: number;
searchStartYear?: string;
searchSido?: string;
searchGugun?: string;
}): BnsSearchDto {
const dto: BnsSearchDto = { cPage: input.cPage };
const year = input.searchStartYear?.trim();
const sido = input.searchSido?.trim();
const gugun = input.searchGugun?.trim();
if (year) dto.searchStartYear = year;
if (sido) dto.searchSido = sido;
if (gugun) dto.searchGugun = gugun;
return dto;
}
@@ -0,0 +1,125 @@
/** 백엔드 BnsSearchDto */
export interface BnsSearchDto {
searchStartYear?: string;
cPage: number;
searchSido?: string;
searchGugun?: string;
}
/** 백엔드 BnsListResponseWrapperDto */
export interface BnsListResponseWrapperDto {
list: BnsListResponseDto[];
cnt: number;
}
/** 백엔드 BnsListResponseDto */
export interface BnsListResponseDto {
cPage: number;
gbn: number;
chk: number;
seqNo: string;
rgnNm: string;
sido: string;
gugun: string;
bzCd: string;
bzNm: string;
yr: string;
strtYr: string;
edYr: string;
totBzamt: string;
}
/** 백엔드 BnsDetailResponseWrapperDto */
export interface BnsDetailResponseWrapperDto {
saup: unknown;
detail: BnsDetailResponseDto;
}
/** 백엔드 BnsDetailResponseDto */
export interface BnsDetailResponseDto {
cPage: number;
seqNo: string;
confBzCd: string;
chngDgr: string;
yr: string;
sido: string;
gugun: string;
rvbsnNm: string;
lgbsnNm: string;
mdmbsnNm: string;
lclgvrnmntNm: string;
accKdNm: string;
bzKdNm: string;
untBzNm: string;
dtlBzNm: string;
mevBzNm: string;
ddtlBzNm: string;
strtYr: string;
edYr: string;
aidRt: string;
bzStatus: string;
bzCd: string;
bzNm: string;
bzLoc: string;
efflWqltStndrdNm: string;
goalYear: string;
bssProm: string;
uppPlan: string;
techInsp: string;
bssCont: string;
rmk: string;
inflBscFctNm: string;
inflBscFctCd: string;
trtMtd: string;
swrExtns: string;
swrChng: string;
swrRnvt: string;
totFamNum: string;
pumpCnt: string;
rvWbsn: string;
rvText: string;
imprvExpctFlw: string;
imprvExpctWqlt: string;
trtplntPlEfflWqlt: string;
wqltAvg: string;
wqltHgh: string;
wqltLow: string;
stdCost: string;
bzEtc: string;
bscFctCd: string;
bscFctNm: string;
fctCpc: string;
fctCpcAfr: string;
trtDstrArea: string;
trtDstrAreaAfr: string;
swrTrtPopl: string;
swrTrtPoplAfr: string;
adstPopl: string;
}
/** 백엔드 EmCodeItemDto */
export interface EmCodeItemDto {
code: string;
data: string;
upCode: string;
}
/** 백엔드 EmCodeListResponseDto */
export interface EmCodeListResponseDto {
rvbsnList: EmCodeItemDto[];
lgbsnList: EmCodeItemDto[];
mdmbsnList: EmCodeItemDto[];
accList: EmCodeItemDto[];
bzList: EmCodeItemDto[];
untList: EmCodeItemDto[];
dtlList: EmCodeItemDto[];
mevList: EmCodeItemDto[];
ddtlList: EmCodeItemDto[];
}
/** Controller public 메서드명·시그니처와 동일 */
export interface BnsIfService {
getBnsList(params: BnsSearchDto): Promise<BnsListResponseWrapperDto>;
getBnsDetail(gbn: string, yr: string, seqNo: string): Promise<BnsDetailResponseWrapperDto>;
getEmCodeList(dpmCd?: string, lclgvrnmntCd?: string): Promise<EmCodeListResponseDto>;
}
@@ -11,33 +11,25 @@ import type {
CklstArtclUpdateRequest,
} from "../model/cklst-artcl-interface";
const BASE = ENDPOINT.AS_SERVICE.CKLST_ARTCL;
async function findPage(params: CklstArtclSearchParams): Promise<PageResponse<CklstArtcl>> {
const response = await client.get<PageResponse<CklstArtcl>>(
ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL,
{ params },
);
const response = await client.get<PageResponse<CklstArtcl>>(BASE, { params });
return response.data;
}
async function findByCklstSn(cklstSn: number): Promise<CklstArtcl[]> {
const response = await client.get<CklstArtcl[]>(
`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL}/by-cklst/${cklstSn}`,
);
const response = await client.get<CklstArtcl[]>(`${BASE}/by-cklst/${cklstSn}`);
return response.data;
}
async function findById(cklstArtclSn: number, cklstSn: number): Promise<CklstArtcl> {
const response = await client.get<CklstArtcl>(
`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL}/${cklstArtclSn}/${cklstSn}`,
);
const response = await client.get<CklstArtcl>(`${BASE}/${cklstArtclSn}/${cklstSn}`);
return response.data;
}
async function create(body: CklstArtclSaveRequest): Promise<CklstArtclId> {
const response = await client.post<CklstArtclId>(
ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL,
body,
);
const response = await client.post<CklstArtclId>(BASE, body);
return response.data;
}
@@ -46,17 +38,12 @@ async function update(
cklstSn: number,
body: CklstArtclUpdateRequest,
): Promise<CklstArtclId> {
const response = await client.put<CklstArtclId>(
`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL}/${cklstArtclSn}/${cklstSn}`,
body,
);
const response = await client.put<CklstArtclId>(`${BASE}/${cklstArtclSn}/${cklstSn}`, body);
return response.data;
}
async function remove(cklstArtclSn: number, cklstSn: number): Promise<CklstArtclId> {
const response = await client.delete<CklstArtclId>(
`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL}/${cklstArtclSn}/${cklstSn}`,
);
const response = await client.delete<CklstArtclId>(`${BASE}/${cklstArtclSn}/${cklstSn}`);
return response.data;
}
@@ -11,40 +11,39 @@ import type {
CklstTreeSearchParams,
} from "../model/cklst-mng-interface";
const BASE = ENDPOINT.AS_SERVICE.CKLST;
async function findPage(params: CklstMngSearchParams): Promise<PageResponse<CklstMng>> {
const response = await client.get<PageResponse<CklstMng>>(ENDPOINT.AS_CHECKLIST_SERVICE.CKLST, {
const response = await client.get<PageResponse<CklstMng>>(BASE, {
params,
});
return response.data;
}
async function findById(cklstSn: number): Promise<CklstMng> {
const response = await client.get<CklstMng>(`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST}/${cklstSn}`);
const response = await client.get<CklstMng>(`${BASE}/${cklstSn}`);
return response.data;
}
async function findTree(params: CklstTreeSearchParams): Promise<CklstTreeNode[]> {
const response = await client.get<CklstTreeNode[]>(`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST}/list/tree`, {
const response = await client.get<CklstTreeNode[]>(`${BASE}/list/tree`, {
params,
});
return response.data;
}
async function create(body: CklstMngSaveRequest): Promise<number> {
const response = await client.post<number>(ENDPOINT.AS_CHECKLIST_SERVICE.CKLST, body);
const response = await client.post<number>(BASE, body);
return response.data;
}
async function update(cklstSn: number, body: CklstMngSaveRequest): Promise<number> {
const response = await client.put<number>(
`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST}/${cklstSn}`,
body,
);
const response = await client.put<number>(`${BASE}/${cklstSn}`, body);
return response.data;
}
async function remove(cklstSn: number): Promise<number> {
const response = await client.delete<number>(`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST}/${cklstSn}`);
const response = await client.delete<number>(`${BASE}/${cklstSn}`);
return response.data;
}
@@ -11,18 +11,15 @@ import type {
CklstRvwUpdateRequest,
} from "../model/cklst-rvw-interface";
const BASE = ENDPOINT.AS_SERVICE.CKLST_RVW;
async function findPage(params: CklstRvwSearchParams): Promise<PageResponse<CklstRvw>> {
const response = await client.get<PageResponse<CklstRvw>>(
ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_RVW,
{ params },
);
const response = await client.get<PageResponse<CklstRvw>>(BASE, { params });
return response.data;
}
async function findByArtcl(cklstSn: number, cklstArtclSn: number): Promise<CklstRvw[]> {
const response = await client.get<CklstRvw[]>(
`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_RVW}/by-artcl/${cklstSn}/${cklstArtclSn}`,
);
const response = await client.get<CklstRvw[]>(`${BASE}/by-artcl/${cklstSn}/${cklstArtclSn}`);
return response.data;
}
@@ -31,17 +28,12 @@ async function findById(
cklstArtclSn: number,
cklstSn: number,
): Promise<CklstRvw> {
const response = await client.get<CklstRvw>(
`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_RVW}/${cklstRvwSn}/${cklstArtclSn}/${cklstSn}`,
);
const response = await client.get<CklstRvw>(`${BASE}/${cklstRvwSn}/${cklstArtclSn}/${cklstSn}`);
return response.data;
}
async function create(body: CklstRvwSaveRequest): Promise<CklstRvwId> {
const response = await client.post<CklstRvwId>(
ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_RVW,
body,
);
const response = await client.post<CklstRvwId>(BASE, body);
return response.data;
}
@@ -52,7 +44,7 @@ async function update(
body: CklstRvwUpdateRequest,
): Promise<CklstRvwId> {
const response = await client.put<CklstRvwId>(
`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_RVW}/${cklstRvwSn}/${cklstArtclSn}/${cklstSn}`,
`${BASE}/${cklstRvwSn}/${cklstArtclSn}/${cklstSn}`,
body,
);
return response.data;
@@ -64,7 +56,7 @@ async function remove(
cklstSn: number,
): Promise<CklstRvwId> {
const response = await client.delete<CklstRvwId>(
`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_RVW}/${cklstRvwSn}/${cklstArtclSn}/${cklstSn}`,
`${BASE}/${cklstRvwSn}/${cklstArtclSn}/${cklstSn}`,
);
return response.data;
}
+3 -3
View File
@@ -18,9 +18,9 @@ import { cklstArtclService } from "./cklst-artcl-service";
import { cklstMngService } from "./cklst-mng-service";
import { cklstRvwService } from "./cklst-rvw-service";
const cklstMngBaseKey = [ENDPOINT.AS_CHECKLIST_SERVICE.CKLST] as const;
const cklstArtclBaseKey = [ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL] as const;
const cklstRvwBaseKey = [ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_RVW] as const;
const cklstMngBaseKey = [ENDPOINT.AS_SERVICE.CKLST] as const;
const cklstArtclBaseKey = [ENDPOINT.AS_SERVICE.CKLST_ARTCL] as const;
const cklstRvwBaseKey = [ENDPOINT.AS_SERVICE.CKLST_RVW] as const;
export function useCklstMngPage(params: CklstMngSearchParams) {
return useQuery({
@@ -12,7 +12,7 @@ import {
type CnstpSeMngTreeDto,
} from "../model/cnstp-interface";
const BASE = ENDPOINT.AS_COMMON_SERVICE.CNSTP_SE;
const BASE = ENDPOINT.AS_SERVICE.CNSTP_SE;
async function findAll(params: CnstpSeMngSearchDto): Promise<PageResponse<CnstpSeMngResponseDto>> {
const response = await client.get<PageResponse<CnstpSeMngResponseDto>>(BASE, { params });
+1 -1
View File
@@ -10,7 +10,7 @@ import type {
import { cnstpSeMngService } from "./cnstp-service";
const cnstpSeMngBaseKey = [ENDPOINT.AS_COMMON_SERVICE.CNSTP_SE] as const;
const cnstpSeMngBaseKey = [ENDPOINT.AS_SERVICE.CNSTP_SE] as const;
export function useCnstpSeMngPage(params: CnstpSeMngSearchDto) {
return useQuery({
@@ -16,6 +16,7 @@ export interface CnstpSeMngResponseDto {
cdLvNo: number;
useYn: string;
expln?: string;
sortSeq?: number;
rgtr?: string;
rgtrNm?: string;
mdfr?: string;
@@ -39,6 +40,7 @@ export interface CnstpSeMngSaveRequestDto {
cdLvNo?: number;
useYn?: string;
expln?: string;
sortSeq?: number;
}
/** PUT /{cnstpSeCd} 요청 (mode는 서비스에서 U로 설정) */
@@ -1,17 +1,67 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type {
ElaprDmndResponse,
ElaprDmndSaveRequest,
ElaprDmndResponseDto,
ElaprDmndSaveRequestDto,
ElaprDmndSearchDto,
ElaprDmndService,
ElaprDocPrgrsResponseDto,
} from "../model/elapr-dmnd-interface";
async function create(body: ElaprDmndSaveRequest): Promise<ElaprDmndResponse> {
const response = await client.post<ElaprDmndResponse>(ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, body);
const BASE = ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND;
async function findPage(params: ElaprDmndSearchDto): Promise<PageResponse<ElaprDmndResponseDto>> {
const response = await client.get<PageResponse<ElaprDmndResponseDto>>(BASE, { params });
return response.data;
}
export const ElaprDmndApi: ElaprDmndService = {
create,
};
async function findById(elaprDmndId: string): Promise<ElaprDmndResponseDto> {
const response = await client.get<ElaprDmndResponseDto>(`${BASE}/${elaprDmndId}`);
return response.data;
}
async function save(body: ElaprDmndSaveRequestDto): Promise<ElaprDmndResponseDto> {
const response = await client.post<ElaprDmndResponseDto>(BASE, body);
return response.data;
}
async function update(
elaprDmndId: string,
body: ElaprDmndSaveRequestDto,
): Promise<ElaprDmndResponseDto> {
const response = await client.put<ElaprDmndResponseDto>(`${BASE}/${elaprDmndId}`, body);
return response.data;
}
async function updateStatus(
elaprDmndId: string,
atrzSttsComCd: string,
): Promise<ElaprDmndResponseDto> {
const response = await client.put<ElaprDmndResponseDto>(`${BASE}/${elaprDmndId}/status`, null, {
params: { atrzSttsComCd },
});
return response.data;
}
async function remove(elaprDmndId: string): Promise<void> {
await client.delete(`${BASE}/${elaprDmndId}`);
}
async function findDocPrgrsByDocNo(elaprDocNo: string): Promise<ElaprDocPrgrsResponseDto[]> {
const response = await client.get<ElaprDocPrgrsResponseDto[]>(
`${BASE}/sanc/docprgrs/${elaprDocNo}`,
);
return response.data;
}
export const elaprDmndService = {
findPage,
findById,
save,
update,
updateStatus,
remove,
findDocPrgrsByDocNo,
} satisfies ElaprDmndService;
@@ -1,7 +0,0 @@
import { client } from "@/shared/api";
export async function processElaprPrgrs(elaprPrgrsId: string, atrzPrgrsSttsComCd = "ATRZ_STTS_003"): Promise<void> {
await client.put(`/as-elapr-service/api/v1/elapr-prgrs-stts/${elaprPrgrsId}/process`, {
atrzPrgrsSttsComCd,
});
}
@@ -0,0 +1,104 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type {
ElaprApproverChangeRequestDto,
ElaprApproverInsertRequestDto,
ElaprPrgrsSttsMyDocSearchParams,
ElaprPrgrsSttsResponseDto,
ElaprPrgrsSttsSaveRequestDto,
ElaprPrgrsSttsService,
ElaprProcessRequestDto,
} from "../model/elapr-prgrs-stts-interface";
const BASE = ENDPOINT.AS_ELAPR_SERVICE.ELAPR_PRGRS_STTS;
async function findAllByDmndId(elaprDmndId?: string): Promise<ElaprPrgrsSttsResponseDto[]> {
const response = await client.get<ElaprPrgrsSttsResponseDto[]>(BASE, {
params: elaprDmndId ? { elaprDmndId } : undefined,
});
return response.data;
}
async function findById(elaprPrgrsId: string): Promise<ElaprPrgrsSttsResponseDto> {
const response = await client.get<ElaprPrgrsSttsResponseDto>(`${BASE}/${elaprPrgrsId}`);
return response.data;
}
async function save(
requestDtos: ElaprPrgrsSttsSaveRequestDto[],
): Promise<ElaprPrgrsSttsResponseDto[]> {
const response = await client.post<ElaprPrgrsSttsResponseDto[]>(BASE, requestDtos);
return response.data;
}
async function process(
elaprPrgrsId: string,
body: ElaprProcessRequestDto,
): Promise<ElaprPrgrsSttsResponseDto> {
const response = await client.put<ElaprPrgrsSttsResponseDto>(
`${BASE}/${elaprPrgrsId}/process`,
body,
);
return response.data;
}
async function processByDocNoAndAprvrUuid(
elaprDocNo: string,
aprvrUuid: string,
body: ElaprProcessRequestDto,
): Promise<ElaprPrgrsSttsResponseDto> {
const response = await client.put<ElaprPrgrsSttsResponseDto>(
`${BASE}/doc/${elaprDocNo}/aprvr/${aprvrUuid}/process`,
body,
);
return response.data;
}
async function deletePrgrsStts(elaprPrgrsId: string): Promise<void> {
await client.delete(`${BASE}/${elaprPrgrsId}`);
}
async function updateApprover(
elaprPrgrsId: string,
body: ElaprApproverChangeRequestDto,
): Promise<ElaprPrgrsSttsResponseDto> {
const response = await client.put<ElaprPrgrsSttsResponseDto>(
`${BASE}/${elaprPrgrsId}/approver`,
body,
);
return response.data;
}
async function insertApprover(
elaprDmndId: string,
body: ElaprApproverInsertRequestDto,
): Promise<ElaprPrgrsSttsResponseDto> {
const response = await client.post<ElaprPrgrsSttsResponseDto>(
`${BASE}/${elaprDmndId}/approver/insert`,
body,
);
return response.data;
}
async function findMyElaperDocList(
params: ElaprPrgrsSttsMyDocSearchParams,
): Promise<PageResponse<ElaprPrgrsSttsResponseDto>> {
const response = await client.get<PageResponse<ElaprPrgrsSttsResponseDto>>(`${BASE}/my/doc`, {
params,
});
return response.data;
}
export const elaprPrgrsSttsService = {
findAllByDmndId,
findById,
save,
process,
processByDocNoAndAprvrUuid,
delete: deletePrgrsStts,
updateApprover,
insertApprover,
findMyElaperDocList,
} satisfies ElaprPrgrsSttsService;
+218 -32
View File
@@ -8,6 +8,14 @@ import type {
ElaprAplnSaveRequest,
ElaprAplnUpdateRequest,
} from "../model/elapr-apln-interface";
import type { ElaprDmndSaveRequestDto, ElaprDmndSearchDto } from "../model/elapr-dmnd-interface";
import type {
ElaprApproverChangeRequestDto,
ElaprApproverInsertRequestDto,
ElaprPrgrsSttsMyDocSearchParams,
ElaprPrgrsSttsSaveRequestDto,
ElaprProcessRequestDto,
} from "../model/elapr-prgrs-stts-interface";
import type {
ElaprTmpltSaveRequest,
ElaprTmpltSearchParams,
@@ -15,10 +23,19 @@ import type {
} from "../model/elapr-tmplt-interface";
import { elaprAplnService } from "./elapr-apln-service";
import { elaprDmndService } from "./elapr-dmnd-service";
import { elaprPrgrsSttsService } from "./elapr-prgrs-stts-service";
import { elaprTmpltService } from "./elapr-tmplt-service";
const elaprTmpltBaseKey = [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT] as const;
const elaprAplnBaseKey = [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN] as const;
const elaprDmndBaseKey = [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND] as const;
const elaprPrgrsSttsBaseKey = [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_PRGRS_STTS] as const;
function invalidateElaprPrgrsSttsQueries(queryClient: ReturnType<typeof useQueryClient>) {
queryClient.invalidateQueries({ queryKey: elaprPrgrsSttsBaseKey });
queryClient.invalidateQueries({ queryKey: elaprDmndBaseKey });
}
export function useElaprTmpltPage(params: ElaprTmpltSearchParams) {
return useQuery({
@@ -50,13 +67,8 @@ export function useUpdateElaprTmplt() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
elaprTmpltId,
body,
}: {
elaprTmpltId: string;
body: ElaprTmpltUpdateRequest;
}) => elaprTmpltService.update(elaprTmpltId, body),
mutationFn: ({ elaprTmpltId, body }: { elaprTmpltId: string; body: ElaprTmpltUpdateRequest }) =>
elaprTmpltService.update(elaprTmpltId, body),
onSuccess: (_data, { elaprTmpltId }) => {
queryClient.invalidateQueries({ queryKey: elaprTmpltBaseKey });
queryClient.invalidateQueries({
@@ -99,12 +111,8 @@ export function useSaveMultiElaprApln() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
body,
}: {
body: ElaprAplnSaveRequest[];
elaprTmpltId: string;
}) => elaprAplnService.saveMulti(body),
mutationFn: ({ body }: { body: ElaprAplnSaveRequest[]; elaprTmpltId: string }) =>
elaprAplnService.saveMulti(body),
onSuccess: (_data, { elaprTmpltId }) => {
queryClient.invalidateQueries({ queryKey: elaprAplnBaseKey });
queryClient.invalidateQueries({
@@ -118,13 +126,8 @@ export function useUpdateElaprApln() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
aplnId,
body,
}: {
aplnId: string;
body: ElaprAplnUpdateRequest;
}) => elaprAplnService.update(aplnId, body),
mutationFn: ({ aplnId, body }: { aplnId: string; body: ElaprAplnUpdateRequest }) =>
elaprAplnService.update(aplnId, body),
onSuccess: (_data, { body, aplnId }) => {
queryClient.invalidateQueries({ queryKey: elaprAplnBaseKey });
queryClient.invalidateQueries({
@@ -141,12 +144,8 @@ export function useDeleteElaprApln() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
aplnId,
}: {
aplnId: string;
elaprTmpltId: string;
}) => elaprAplnService.remove(aplnId),
mutationFn: ({ aplnId }: { aplnId: string; elaprTmpltId: string }) =>
elaprAplnService.remove(aplnId),
onSuccess: (_data, { elaprTmpltId }) => {
queryClient.invalidateQueries({ queryKey: elaprAplnBaseKey });
queryClient.invalidateQueries({
@@ -160,12 +159,8 @@ export function useReorderElaprApln() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
body,
}: {
body: ElaprAplnReorderRequest;
elaprTmpltId: string;
}) => elaprAplnService.reorder(body),
mutationFn: ({ body }: { body: ElaprAplnReorderRequest; elaprTmpltId: string }) =>
elaprAplnService.reorder(body),
onSuccess: (_data, { elaprTmpltId }) => {
queryClient.invalidateQueries({ queryKey: elaprAplnBaseKey });
queryClient.invalidateQueries({
@@ -174,3 +169,194 @@ export function useReorderElaprApln() {
},
});
}
export function useElaprDmndPage(params: ElaprDmndSearchDto) {
return useQuery({
queryKey: [...elaprDmndBaseKey, "findPage", params],
queryFn: () => elaprDmndService.findPage(params),
});
}
export function useElaprDmnd(elaprDmndId: string | null | undefined) {
return useQuery({
queryKey: [...elaprDmndBaseKey, "findById", elaprDmndId],
queryFn: () => elaprDmndService.findById(elaprDmndId!),
enabled: Boolean(elaprDmndId),
});
}
export function useElaprDocPrgrsByDocNo(elaprDocNo: string | null | undefined) {
return useQuery({
queryKey: [...elaprDmndBaseKey, "findDocPrgrsByDocNo", elaprDocNo],
queryFn: () => elaprDmndService.findDocPrgrsByDocNo(elaprDocNo!),
enabled: Boolean(elaprDocNo),
});
}
export function useSaveElaprDmnd() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (body: ElaprDmndSaveRequestDto) => elaprDmndService.save(body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: elaprDmndBaseKey });
},
});
}
export function useUpdateElaprDmnd() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ elaprDmndId, body }: { elaprDmndId: string; body: ElaprDmndSaveRequestDto }) =>
elaprDmndService.update(elaprDmndId, body),
onSuccess: (_data, { elaprDmndId }) => {
queryClient.invalidateQueries({ queryKey: elaprDmndBaseKey });
queryClient.invalidateQueries({
queryKey: [...elaprDmndBaseKey, "findById", elaprDmndId],
});
},
});
}
export function useUpdateElaprDmndStatus() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ elaprDmndId, atrzSttsComCd }: { elaprDmndId: string; atrzSttsComCd: string }) =>
elaprDmndService.updateStatus(elaprDmndId, atrzSttsComCd),
onSuccess: (_data, { elaprDmndId }) => {
queryClient.invalidateQueries({ queryKey: elaprDmndBaseKey });
queryClient.invalidateQueries({
queryKey: [...elaprDmndBaseKey, "findById", elaprDmndId],
});
},
});
}
export function useRemoveElaprDmnd() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (elaprDmndId: string) => elaprDmndService.remove(elaprDmndId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: elaprDmndBaseKey });
},
});
}
export function useElaprPrgrsSttsByDmndId(elaprDmndId: string | null | undefined) {
return useQuery({
queryKey: [...elaprPrgrsSttsBaseKey, "findAllByDmndId", elaprDmndId],
queryFn: () => elaprPrgrsSttsService.findAllByDmndId(elaprDmndId!),
enabled: Boolean(elaprDmndId),
});
}
export function useElaprPrgrsStts(elaprPrgrsId: string | null | undefined) {
return useQuery({
queryKey: [...elaprPrgrsSttsBaseKey, elaprPrgrsId],
queryFn: () => elaprPrgrsSttsService.findById(elaprPrgrsId!),
enabled: Boolean(elaprPrgrsId),
});
}
export function useElaprPrgrsSttsMyDocPage(params: ElaprPrgrsSttsMyDocSearchParams) {
return useQuery({
queryKey: [...elaprPrgrsSttsBaseKey, "findMyElaperDocList", params],
queryFn: () => elaprPrgrsSttsService.findMyElaperDocList(params),
});
}
export function useSaveElaprPrgrsStts() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (requestDtos: ElaprPrgrsSttsSaveRequestDto[]) =>
elaprPrgrsSttsService.save(requestDtos),
onSuccess: () => {
invalidateElaprPrgrsSttsQueries(queryClient);
},
});
}
export function useProcessElaprPrgrsStts() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
elaprPrgrsId,
body,
}: {
elaprPrgrsId: string;
body: ElaprProcessRequestDto;
}) => elaprPrgrsSttsService.process(elaprPrgrsId, body),
onSuccess: () => {
invalidateElaprPrgrsSttsQueries(queryClient);
},
});
}
export function useProcessElaprPrgrsSttsByDoc() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
elaprDocNo,
aprvrUuid,
body,
}: {
elaprDocNo: string;
aprvrUuid: string;
body: ElaprProcessRequestDto;
}) => elaprPrgrsSttsService.processByDocNoAndAprvrUuid(elaprDocNo, aprvrUuid, body),
onSuccess: () => {
invalidateElaprPrgrsSttsQueries(queryClient);
},
});
}
export function useDeleteElaprPrgrsStts() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (elaprPrgrsId: string) => elaprPrgrsSttsService.delete(elaprPrgrsId),
onSuccess: () => {
invalidateElaprPrgrsSttsQueries(queryClient);
},
});
}
export function useUpdateElaprPrgrsSttsApprover() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
elaprPrgrsId,
body,
}: {
elaprPrgrsId: string;
body: ElaprApproverChangeRequestDto;
}) => elaprPrgrsSttsService.updateApprover(elaprPrgrsId, body),
onSuccess: () => {
invalidateElaprPrgrsSttsQueries(queryClient);
},
});
}
export function useInsertElaprPrgrsSttsApprover() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
elaprDmndId,
body,
}: {
elaprDmndId: string;
body: ElaprApproverInsertRequestDto;
}) => elaprPrgrsSttsService.insertApprover(elaprDmndId, body),
onSuccess: () => {
invalidateElaprPrgrsSttsQueries(queryClient);
},
});
}
+122 -33
View File
@@ -1,33 +1,122 @@
export { elaprAplnService } from "./api/elapr-apln-service";
export { elaprTmpltService } from "./api/elapr-tmplt-service";
export {
useCreateElaprTmplt,
useDeleteElaprApln,
useDeleteElaprTmplt,
useElaprApln,
useElaprAplnPage,
useElaprTmplt,
useElaprTmpltPage,
useReorderElaprApln,
useSaveMultiElaprApln,
useUpdateElaprApln,
useUpdateElaprTmplt,
} from "./api/queries";
export {
ELAPR_APLN_SAVE_MODE,
type ElaprApln,
type ElaprAplnListParams,
type ElaprAplnReorderItem,
type ElaprAplnReorderRequest,
type ElaprAplnSaveMode,
type ElaprAplnSaveRequest,
type ElaprAplnService,
type ElaprAplnUpdateRequest,
} from "./model/elapr-apln-interface";
export type {
ElaprTmplt,
ElaprTmpltSaveRequest,
ElaprTmpltSearchParams,
ElaprTmpltService,
ElaprTmpltUpdateRequest,
} from "./model/elapr-tmplt-interface";
export { elaprAplnService } from "./api/elapr-apln-service";
export { elaprDmndService } from "./api/elapr-dmnd-service";
export { elaprPrgrsSttsService } from "./api/elapr-prgrs-stts-service";
export { elaprTmpltService } from "./api/elapr-tmplt-service";
export {
useCreateElaprTmplt,
useDeleteElaprApln,
useDeleteElaprPrgrsStts,
useDeleteElaprTmplt,
useElaprApln,
useElaprAplnPage,
useElaprDmnd,
useElaprDmndPage,
useElaprDocPrgrsByDocNo,
useElaprPrgrsStts,
useElaprPrgrsSttsByDmndId,
useElaprPrgrsSttsMyDocPage,
useElaprTmplt,
useElaprTmpltPage,
useInsertElaprPrgrsSttsApprover,
useProcessElaprPrgrsStts,
useProcessElaprPrgrsSttsByDoc,
useRemoveElaprDmnd,
useReorderElaprApln,
useSaveElaprDmnd,
useSaveElaprPrgrsStts,
useSaveMultiElaprApln,
useUpdateElaprApln,
useUpdateElaprDmnd,
useUpdateElaprDmndStatus,
useUpdateElaprPrgrsSttsApprover,
useUpdateElaprTmplt,
} from "./api/queries";
export {
assignAtrzSeqNumbers,
buildSaveMultiPayload,
hasUnsavedChanges,
} from "./lib/apln-save-utils";
export {
getVisibleRows,
validateAllRowsForSave,
validateAplnRow,
validateAtrzOrder,
validateUniqueSeqNo,
} from "./lib/apln-validators";
export {
findCurrentUserPendingApprovalRequest,
hasCurrentUserPendingApprovalRequest,
} from "./lib/approval-request-utils";
export {
APPROVER_STATUS_MAP,
type ApproverStatusConfig,
getApproverStatus,
sortByAtrzSeqNo,
} from "./lib/approval-utils";
export {
buildElaprPrgrsSttsListFromApln,
buildElaprPrgrsSttsListFromAplnRows,
} from "./lib/dmnd-save-utils";
export type { ElaprPrgrsSttsRow } from "./lib/prgrs-stts-save-utils";
export {
assignPrgrsSttsAtrzSeqNumbers,
buildPrgrsSttsSavePayload,
hasUnsavedPrgrsSttsChanges,
isPrgrsSttsRowEditable,
PRGRS_STTS_WAITING_CD,
validateAllPrgrsSttsRows,
validatePrgrsSttsRow,
} from "./lib/prgrs-stts-save-utils";
export { createDraftRowKey, useAplnRows } from "./lib/use-apln-rows";
export { useDndSensors } from "./lib/use-dnd-sensors";
export { usePrgrsSttsRows } from "./lib/use-prgrs-stts-rows";
export {
ELAPR_APLN_SAVE_MODE,
type ElaprApln,
type ElaprAplnListParams,
type ElaprAplnReorderItem,
type ElaprAplnReorderRequest,
type ElaprAplnSaveMode,
type ElaprAplnSaveRequest,
type ElaprAplnService,
type ElaprAplnUpdateRequest,
} from "./model/elapr-apln-interface";
export type { ElaprAplnRow } from "./model/elapr-apln-row-types";
export type {
ElaprDmndResponseDto,
ElaprDmndSaveRequestDto,
ElaprDmndSearchDto,
ElaprDmndService,
ElaprDocPrgrsResponseDto,
} from "./model/elapr-dmnd-interface";
export type {
ElaprApproverChangeRequestDto,
ElaprApproverInsertRequestDto,
ElaprPrgrsSttsMyDocSearchParams,
ElaprPrgrsSttsResponseDto,
ElaprPrgrsSttsSaveMode,
ElaprPrgrsSttsSaveRequestDto,
ElaprPrgrsSttsSearchDto,
ElaprPrgrsSttsService,
ElaprProcessRequestDto,
} from "./model/elapr-prgrs-stts-interface";
export { ELAPR_PRGRS_STTS_SAVE_MODE } from "./model/elapr-prgrs-stts-interface";
export type {
ElaprTmplt,
ElaprTmpltSaveRequest,
ElaprTmpltSearchParams,
ElaprTmpltService,
ElaprTmpltUpdateRequest,
} from "./model/elapr-tmplt-interface";
export type { ReviewerOption } from "./model/reviewer-option";
export { AprvrPickerDialog } from "./ui/aprvr-picker-dialog";
export { ElaprAplnPickerGrid } from "./ui/elapr-apln-picker-grid";
export { ElaprApprovalDialog } from "./ui/elapr-approval-dialog";
export { ElaprDocPrgrsStatus } from "./ui/elapr-doc-prgrs-status";
export {
type ElaprTmpltPickerConfirmPayload,
ElaprTmpltPickerDialog,
} from "./ui/elapr-tmplt-picker-dialog";
export { ElaprTmpltSelectTable } from "./ui/elapr-tmplt-select-table";
export { SortableAplnPickerRow } from "./ui/sortable-apln-picker-row";
export { SortablePrgrsSttsRow } from "./ui/sortable-prgrs-stts-row";
@@ -1,8 +1,16 @@
import { ELAPR_APLN_SAVE_MODE, type ElaprApln, type ElaprAplnSaveRequest } from "@/entities/elapr";
import {
ELAPR_APLN_SAVE_MODE,
type ElaprApln,
type ElaprAplnSaveRequest,
} from "../model/elapr-apln-interface";
import type { ElaprAplnRow } from "../model/elapr-apln-row-types";
import { assignAtrzSeqNumbers } from "./assign-atrz-seq";
import { getVisibleRows } from "./validators";
import type { ElaprAplnRow } from "../model/apln-row-types";
import { getVisibleRows } from "./apln-validators";
/** 그리드 행 순서(1-based)로 결재순서번호 일괄 채번 */
export function assignAtrzSeqNumbers(rows: ElaprAplnRow[]): ElaprAplnRow[] {
return rows.map((row, index) => ({ ...row, atrzSeqNo: index + 1 }));
}
function rowDiffersFromServer(row: ElaprAplnRow, serverRow: ElaprApln): boolean {
return (
@@ -22,6 +30,25 @@ function shouldIncludeUpdate(row: ElaprAplnRow, serverRows: ElaprApln[]): boolea
return rowDiffersFromServer(row, serverRow);
}
export function hasUnsavedChanges(rows: ElaprAplnRow[], serverRows: ElaprApln[]): boolean {
if (rows.some((row) => row.isDeleted && row.aplnId)) return true;
if (rows.some((row) => row.isNew && !row.isDeleted)) return true;
const visibleWithSeq = assignAtrzSeqNumbers(getVisibleRows(rows));
for (const row of visibleWithSeq) {
if (row.isNew) continue;
if (!row.aplnId) continue;
if (row.isDirty) return true;
const serverRow = serverRows.find((s) => s.aplnId === row.aplnId);
if (!serverRow) continue;
if (rowDiffersFromServer(row, serverRow)) return true;
}
return false;
}
export function buildSaveMultiPayload(
rows: ElaprAplnRow[],
elaprTmpltId: string,
@@ -1,23 +1,19 @@
import type { ElaprAplnRow } from "../model/apln-row-types";
import type { ElaprAplnRow } from "../model/elapr-apln-row-types";
const ATRZ_RECEIVER_CD = "ATRZ01";
const ATRZ_FINAL_CD = "ATRZ99";
interface AtrzOrderValidateRow {
atrzSeComCd?: string;
isDeleted?: boolean;
}
export function getVisibleRows(rows: ElaprAplnRow[]): ElaprAplnRow[] {
return rows.filter((row) => !row.isDeleted);
}
/** 접수자·최종승인자 위치·중복 검증 (최종 저장 시점) */
export function validateAtrzOrder(rows: ElaprAplnRow[]): string | null {
const visibleRows = getVisibleRows(rows);
const receiverCount = visibleRows.filter((row) => row.atrzSeComCd === ATRZ_RECEIVER_CD).length;
if (receiverCount === 0) {
return "접수자는 반드시 등록해야 합니다.";
}
if (receiverCount > 1) {
return "접수자는 한 명만 등록할 수 있습니다.";
}
export function validateAtrzOrder(rows: AtrzOrderValidateRow[]): string | null {
const visibleRows = rows.filter((row) => !row.isDeleted);
const finalCount = visibleRows.filter((row) => row.atrzSeComCd === ATRZ_FINAL_CD).length;
if (finalCount === 0) {
@@ -27,10 +23,6 @@ export function validateAtrzOrder(rows: ElaprAplnRow[]): string | null {
return "최종승인자는 한 명만 등록할 수 있습니다.";
}
if (receiverCount === 1 && visibleRows[0]?.atrzSeComCd !== ATRZ_RECEIVER_CD) {
return "접수자는 결재선 첫 번째에 배치해야 합니다.";
}
if (finalCount === 1) {
const lastRow = visibleRows[visibleRows.length - 1];
if (lastRow?.atrzSeComCd !== ATRZ_FINAL_CD) {
@@ -0,0 +1,23 @@
import type { ElaprDocPrgrsResponseDto } from "../model/elapr-dmnd-interface";
export function hasCurrentUserPendingApprovalRequest(
list: ElaprDocPrgrsResponseDto[],
currentUserUuid: string | undefined,
): boolean {
if (!currentUserUuid) return false;
return list.some(
(row) => row.aprvrUuid === currentUserUuid && row.atrzPrgrsSttsComCd === "ATRZ_PRGRS_002",
);
}
export function findCurrentUserPendingApprovalRequest(
list?: ElaprDocPrgrsResponseDto[],
currentUserUuid?: string | undefined,
): ElaprDocPrgrsResponseDto | undefined {
if (!currentUserUuid) return undefined;
return list?.find(
(row) => row.aprvrUuid === currentUserUuid && row.atrzPrgrsSttsComCd === "ATRZ_PRGRS_002",
);
}
@@ -0,0 +1,29 @@
import type { ElaprDocPrgrsClientResponseDto } from "@/shared/model";
export interface ApproverStatusConfig {
label: string;
badgeClass: string;
}
export const APPROVER_STATUS_MAP: Record<string, ApproverStatusConfig> = {
ATRZ_PRGRS_001: { label: "대기", badgeClass: "bg-gray-100 text-gray-700" },
ATRZ_PRGRS_002: { label: "요청", badgeClass: "bg-yellow-100 text-yellow-700" },
ATRZ_PRGRS_003: { label: "승인", badgeClass: "bg-green-100 text-green-700" },
ATRZ_PRGRS_004: { label: "반려", badgeClass: "bg-red-100 text-red-700" },
ATRZ_PRGRS_999: { label: "결재안함", badgeClass: "bg-blue-100 text-blue-700" },
};
export function sortByAtrzSeqNo(
list: ElaprDocPrgrsClientResponseDto[],
): ElaprDocPrgrsClientResponseDto[] {
return list.slice().sort((a, b) => Number(a.atrzSeqNo ?? 0) - Number(b.atrzSeqNo ?? 0));
}
export function getApproverStatus(status: string | undefined): ApproverStatusConfig {
return (
APPROVER_STATUS_MAP[status ?? ""] ?? {
label: status || "-",
badgeClass: "bg-gray-100 text-gray-700",
}
);
}
@@ -0,0 +1,43 @@
import type { ElaprApln } from "../model/elapr-apln-interface";
import type { ElaprAplnRow } from "../model/elapr-apln-row-types";
import type { ElaprPrgrsSttsSaveRequestDto } from "../model/elapr-prgrs-stts-interface";
import { getVisibleRows } from "./apln-validators";
function toPrgrsSttsItem(
aprvrUuid: string | undefined,
atrzSeComCd: string | undefined,
atrzSeqNo: number | undefined,
): ElaprPrgrsSttsSaveRequestDto {
if (!aprvrUuid?.trim()) {
throw new Error("결재자가 지정되지 않은 행이 있습니다.");
}
if (!atrzSeComCd?.trim()) {
throw new Error("결재구분이 지정되지 않은 행이 있습니다.");
}
if (atrzSeqNo == null) {
throw new Error("결재순서번호가 지정되지 않은 행이 있습니다.");
}
return {
aprvrUuid,
atrzSeComCd,
atrzSeqNo,
};
}
/** 서식 결재선 API 응답 → 기안 요청용 진행상태 목록 */
export function buildElaprPrgrsSttsListFromApln(
items: ElaprApln[],
): ElaprPrgrsSttsSaveRequestDto[] {
return items.map((item) => toPrgrsSttsItem(item.aprvrUuid, item.atrzSeComCd, item.atrzSeqNo));
}
/** 결재선 그리드 행 → 기안 요청용 진행상태 목록 (서버가 seq 기준 상태 자동 결정) */
export function buildElaprPrgrsSttsListFromAplnRows(
rows: ElaprAplnRow[],
): ElaprPrgrsSttsSaveRequestDto[] {
return getVisibleRows(rows).map((row) =>
toPrgrsSttsItem(row.aprvrUuid, row.atrzSeComCd, row.atrzSeqNo),
);
}
@@ -0,0 +1,182 @@
import {
ELAPR_PRGRS_STTS_SAVE_MODE,
type ElaprPrgrsSttsSaveRequestDto,
} from "../model/elapr-prgrs-stts-interface";
export const PRGRS_STTS_WAITING_CD = "ATRZ_PRGRS_001";
export const PRGRS_STTS_APPROVED_CD = "ATRZ_PRGRS_003";
/** 서버 행 비교용 최소 필드 */
export interface PrgrsSttsServerRow {
elaprPrgrsId?: string;
aprvrUuid?: string;
atrzSeComCd?: string;
atrzSeqNo?: number;
dcarbYn?: string;
}
/** 결재선 그리드 로컬 행 — 진행상태 API 응답 + UI 보조 필드 */
export interface ElaprPrgrsSttsRow {
rowKey: string;
elaprPrgrsId?: string;
elaprDmndId?: string;
aprvrUuid?: string;
aprvrNm?: string;
atrzSeComCd?: string;
atrzSeComNm?: string;
atrzSeqNo?: number;
atrzPrgrsSttsComCd?: string;
atrzPrgrsSttsComNm?: string;
rjctRsn?: string | null;
ognzNm?: string;
jbpsSeNm?: string;
jbgdSeNm?: string;
dcarbYn?: string;
isNew?: boolean;
isDirty?: boolean;
/** 소프트 삭제 — 최종 저장 시 mode:D */
isDeleted?: boolean;
/** 수정 취소용 스냅샷 */
snapshot?: ElaprPrgrsSttsRow;
}
export function isPrgrsSttsRowEditable(row: ElaprPrgrsSttsRow): boolean {
return row.isNew === true || row.atrzPrgrsSttsComCd === PRGRS_STTS_WAITING_CD;
}
export function isPrgrsSttsRowDcarbable(row: ElaprPrgrsSttsRow): boolean {
return row.atrzPrgrsSttsComCd !== PRGRS_STTS_APPROVED_CD;
}
function getVisibleRows(rows: ElaprPrgrsSttsRow[]): ElaprPrgrsSttsRow[] {
return rows.filter((row) => !row.isDeleted);
}
/** 그리드 행 순서(1-based)로 결재순서번호 일괄 채번 */
export function assignPrgrsSttsAtrzSeqNumbers(rows: ElaprPrgrsSttsRow[]): ElaprPrgrsSttsRow[] {
return rows.map((row, index) => ({ ...row, atrzSeqNo: index + 1 }));
}
export function validatePrgrsSttsRow(row: ElaprPrgrsSttsRow): string | null {
if (!row.atrzSeComCd?.trim()) {
return "결재구분을 선택해 주세요.";
}
if (!row.aprvrUuid?.trim()) {
return "결재자를 선택해 주세요.";
}
return null;
}
export function validateAllPrgrsSttsRows(
rows: ElaprPrgrsSttsRow[],
isEditingLocked: boolean,
): string | null {
if (isEditingLocked) {
return "편집 중인 행을 먼저 저장하거나 취소해 주세요.";
}
const visibleRows = getVisibleRows(rows);
if (visibleRows.length === 0) {
return "저장할 결재선이 없습니다.";
}
for (const row of visibleRows) {
const fieldError = validatePrgrsSttsRow(row);
if (fieldError) return fieldError;
}
return null;
}
function rowDiffersFromServer(row: ElaprPrgrsSttsRow, serverRow: PrgrsSttsServerRow): boolean {
return (
row.aprvrUuid !== serverRow.aprvrUuid ||
row.atrzSeComCd !== serverRow.atrzSeComCd ||
row.atrzSeqNo !== serverRow.atrzSeqNo ||
(row.dcarbYn ?? "N") !== (serverRow.dcarbYn ?? "N")
);
}
function shouldIncludeUpdate(row: ElaprPrgrsSttsRow, serverRows: PrgrsSttsServerRow[]): boolean {
if (row.isDirty) return true;
if (!row.elaprPrgrsId) return false;
const serverRow = serverRows.find((s) => s.elaprPrgrsId === row.elaprPrgrsId);
if (!serverRow) return false;
return rowDiffersFromServer(row, serverRow);
}
export function hasUnsavedPrgrsSttsChanges(
rows: ElaprPrgrsSttsRow[],
serverRows: PrgrsSttsServerRow[],
): boolean {
if (rows.some((row) => row.isDeleted && row.elaprPrgrsId)) return true;
if (rows.some((row) => row.isNew && !row.isDeleted)) return true;
const visibleWithSeq = assignPrgrsSttsAtrzSeqNumbers(getVisibleRows(rows));
for (const row of visibleWithSeq) {
if (row.isNew) continue;
if (!row.elaprPrgrsId) continue;
if (row.isDirty) return true;
const serverRow = serverRows.find((s) => s.elaprPrgrsId === row.elaprPrgrsId);
if (!serverRow) continue;
if (rowDiffersFromServer(row, serverRow)) return true;
}
return false;
}
export function buildPrgrsSttsSavePayload(
rows: ElaprPrgrsSttsRow[],
elaprDmndId: string,
serverRows: PrgrsSttsServerRow[],
): ElaprPrgrsSttsSaveRequestDto[] {
const payload: ElaprPrgrsSttsSaveRequestDto[] = [];
for (const row of rows) {
if (row.isDeleted && row.elaprPrgrsId) {
payload.push({
mode: ELAPR_PRGRS_STTS_SAVE_MODE.DELETE,
elaprPrgrsId: row.elaprPrgrsId,
elaprDmndId,
});
}
}
const visibleWithSeq = assignPrgrsSttsAtrzSeqNumbers(getVisibleRows(rows));
for (const row of visibleWithSeq) {
if (row.isNew) {
payload.push({
mode: ELAPR_PRGRS_STTS_SAVE_MODE.CREATE,
elaprDmndId,
aprvrUuid: row.aprvrUuid,
atrzSeComCd: row.atrzSeComCd,
atrzSeqNo: row.atrzSeqNo,
atrzPrgrsSttsComCd: row.atrzPrgrsSttsComCd,
dcarbYn: row.dcarbYn ?? "N",
});
continue;
}
if (!row.elaprPrgrsId) continue;
if (shouldIncludeUpdate(row, serverRows)) {
payload.push({
mode: ELAPR_PRGRS_STTS_SAVE_MODE.UPDATE,
elaprPrgrsId: row.elaprPrgrsId,
elaprDmndId,
aprvrUuid: row.aprvrUuid,
atrzSeComCd: row.atrzSeComCd,
atrzSeqNo: row.atrzSeqNo,
atrzPrgrsSttsComCd: row.atrzPrgrsSttsComCd,
dcarbYn: row.dcarbYn ?? "N",
});
}
}
return payload;
}
@@ -4,12 +4,11 @@ import { useCallback, useEffect, useState } from "react";
import { arrayMove } from "@dnd-kit/sortable";
import type { ElaprApln } from "@/entities/elapr";
import type { ElaprApln } from "../model/elapr-apln-interface";
import type { ElaprAplnRow } from "../model/elapr-apln-row-types";
import type { ElaprAplnRow } from "../model/apln-row-types";
import { assignAtrzSeqNumbers } from "./assign-atrz-seq";
import { getVisibleRows } from "./validators";
import { assignAtrzSeqNumbers } from "./apln-save-utils";
import { getVisibleRows } from "./apln-validators";
function toRow(item: ElaprApln): ElaprAplnRow {
return {
@@ -101,7 +100,13 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) {
return prev.map((row) => {
if (row.rowKey !== editingRowKey) return row;
if (row.snapshot) {
const { snapshot: _s, isNew: _n, isDirty: _d, isDeleted: _del, ...restored } = row.snapshot;
const {
snapshot: _s,
isNew: _n,
isDirty: _d,
isDeleted: _del,
...restored
} = row.snapshot;
return { ...restored, isNew: false, isDirty: false, isDeleted: false };
}
return { ...row, isDirty: false };
@@ -110,19 +115,16 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) {
setEditingRowKey(null);
}, [editingRowKey]);
const commitEdit = useCallback(
(rowKey: string) => {
setRows((prev) =>
prev.map((row) => {
if (row.rowKey !== rowKey) return row;
const { snapshot: _s, ...committed } = row;
return { ...committed, isDirty: true };
}),
);
setEditingRowKey(null);
},
[],
);
const commitEdit = useCallback((rowKey: string) => {
setRows((prev) =>
prev.map((row) => {
if (row.rowKey !== rowKey) return row;
const { snapshot: _s, ...committed } = row;
return { ...committed, isDirty: true };
}),
);
setEditingRowKey(null);
}, []);
const markDeleted = useCallback((rowKey: string) => {
setRows((prev) => {
@@ -158,7 +160,10 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) {
const visibleKeys = new Set(reorderedVisible.map((r) => r.rowKey));
const deletedRows = prev.filter((r) => r.isDeleted);
const merged = [...reorderedVisible, ...deletedRows.filter((r) => !visibleKeys.has(r.rowKey))];
const merged = [
...reorderedVisible,
...deletedRows.filter((r) => !visibleKeys.has(r.rowKey)),
];
payloadRows = reorderedVisible;
return merged;
@@ -1,3 +1,5 @@
"use client";
import { KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
@@ -0,0 +1,240 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { arrayMove } from "@dnd-kit/sortable";
import type { ElaprDocPrgrsClientResponseDto } from "@/shared/model";
import {
assignPrgrsSttsAtrzSeqNumbers,
type ElaprPrgrsSttsRow,
isPrgrsSttsRowEditable,
PRGRS_STTS_WAITING_CD,
} from "./prgrs-stts-save-utils";
import { createDraftRowKey } from "./use-apln-rows";
function getVisibleRows(rows: ElaprPrgrsSttsRow[]): ElaprPrgrsSttsRow[] {
return rows.filter((row) => !row.isDeleted);
}
function toRow(item: ElaprDocPrgrsClientResponseDto): ElaprPrgrsSttsRow {
return {
rowKey: item.elaprPrgrsId ?? createDraftRowKey(),
elaprPrgrsId: item.elaprPrgrsId,
elaprDmndId: item.elaprDmndId,
aprvrUuid: item.aprvrUuid,
aprvrNm: item.aprvrNm,
atrzSeComCd: item.atrzSeComCd,
atrzSeComNm: item.atrzSeComNm,
atrzSeqNo: item.atrzSeqNo,
atrzPrgrsSttsComCd: item.atrzPrgrsSttsComCd,
atrzPrgrsSttsComNm: item.atrzPrgrsSttsComNm,
rjctRsn: item.rjctRsn,
ognzNm: item.ognzNm,
jbpsSeNm: item.jbpsSeNm,
jbgdSeNm: item.jbgdSeNm,
dcarbYn: item.dcarbYn ?? "N",
isNew: false,
isDirty: false,
isDeleted: false,
};
}
interface UsePrgrsSttsRowsOptions {
serverRows?: ElaprDocPrgrsClientResponseDto[];
elaprDmndId?: string | null;
}
export function usePrgrsSttsRows({ serverRows, elaprDmndId }: UsePrgrsSttsRowsOptions) {
const [rows, setRows] = useState<ElaprPrgrsSttsRow[]>([]);
const [editingRowKey, setEditingRowKey] = useState<string | null>(null);
useEffect(() => {
setRows((serverRows ?? []).map(toRow));
setEditingRowKey(null);
}, [serverRows]);
const isEditingLocked = editingRowKey != null;
const visibleRows = getVisibleRows(rows);
const addRow = useCallback(() => {
if (!elaprDmndId || isEditingLocked) return;
const rowKey = createDraftRowKey();
setRows((prev) => [
...prev,
{
rowKey,
elaprDmndId,
atrzSeComCd: "",
aprvrUuid: "",
aprvrNm: "",
atrzPrgrsSttsComCd: PRGRS_STTS_WAITING_CD,
dcarbYn: "N",
isNew: true,
isDirty: true,
isDeleted: false,
},
]);
setEditingRowKey(rowKey);
}, [elaprDmndId, isEditingLocked]);
const startEdit = useCallback(
(rowKey: string) => {
if (isEditingLocked) return;
const target = rows.find((r) => r.rowKey === rowKey);
if (!target || !isPrgrsSttsRowEditable(target)) return;
setEditingRowKey(rowKey);
setRows((prev) =>
prev.map((row) => (row.rowKey === rowKey ? { ...row, snapshot: { ...row } } : row)),
);
},
[isEditingLocked, rows],
);
const updateRow = useCallback(
(rowKey: string, patch: Partial<ElaprPrgrsSttsRow>) => {
if (editingRowKey !== rowKey) return;
setRows((prev) =>
prev.map((row) => (row.rowKey === rowKey ? { ...row, ...patch, isDirty: true } : row)),
);
},
[editingRowKey],
);
const setDcarbYn = useCallback(
(rowKey: string, checked: boolean) => {
if (editingRowKey != null && editingRowKey !== rowKey) return;
setRows((prev) => {
const target = prev.find((r) => r.rowKey === rowKey);
if (!target || target.isDeleted) return prev;
const nextYn = checked ? "Y" : "N";
return prev.map((row) => {
if (row.isDeleted) return row;
if (row.rowKey === rowKey) {
if ((row.dcarbYn ?? "N") === nextYn) return row;
return { ...row, dcarbYn: nextYn, isDirty: true };
}
if (!checked) return row;
if ((row.dcarbYn ?? "N") === "N") return row;
return { ...row, dcarbYn: "N", isDirty: true };
});
});
},
[editingRowKey],
);
const cancelEdit = useCallback(() => {
if (!editingRowKey) return;
setRows((prev) => {
const target = prev.find((r) => r.rowKey === editingRowKey);
if (target?.isNew) {
return prev.filter((r) => r.rowKey !== editingRowKey);
}
return prev.map((row) => {
if (row.rowKey !== editingRowKey) return row;
if (row.snapshot) {
const {
snapshot: _s,
isNew: _n,
isDirty: _d,
isDeleted: _del,
...restored
} = row.snapshot;
return { ...restored, isNew: false, isDirty: false, isDeleted: false };
}
return { ...row, isDirty: false };
});
});
setEditingRowKey(null);
}, [editingRowKey]);
const commitEdit = useCallback((rowKey: string) => {
setRows((prev) =>
prev.map((row) => {
if (row.rowKey !== rowKey) return row;
const { snapshot: _s, ...committed } = row;
return { ...committed, isDirty: true };
}),
);
setEditingRowKey(null);
}, []);
const markDeleted = useCallback((rowKey: string) => {
setRows((prev) => {
const target = prev.find((r) => r.rowKey === rowKey);
if (!target || !isPrgrsSttsRowEditable(target)) return prev;
if (target.isNew) {
return prev.filter((r) => r.rowKey !== rowKey);
}
return prev.map((row) =>
row.rowKey === rowKey ? { ...row, isDeleted: true, isDirty: true } : row,
);
});
setEditingRowKey((key) => (key === rowKey ? null : key));
}, []);
const reorderRows = useCallback(
(activeKey: string, overKey: string): ElaprPrgrsSttsRow[] => {
if (isEditingLocked) return [];
let payloadRows: ElaprPrgrsSttsRow[] = [];
setRows((prev) => {
const visible = getVisibleRows(prev);
const activeRow = visible.find((r) => r.rowKey === activeKey);
const overRow = visible.find((r) => r.rowKey === overKey);
if (!activeRow || !overRow) return prev;
if (!isPrgrsSttsRowEditable(activeRow) || !isPrgrsSttsRowEditable(overRow)) return prev;
const oldIndex = visible.findIndex((r) => r.rowKey === activeKey);
const newIndex = visible.findIndex((r) => r.rowKey === overKey);
if (oldIndex < 0 || newIndex < 0) return prev;
const reorderedVisible = assignPrgrsSttsAtrzSeqNumbers(
arrayMove(visible, oldIndex, newIndex),
).map((row) => (isPrgrsSttsRowEditable(row) ? { ...row, isDirty: true } : row));
const visibleKeys = new Set(reorderedVisible.map((r) => r.rowKey));
const deletedRows = prev.filter((r) => r.isDeleted);
const merged = [
...reorderedVisible,
...deletedRows.filter((r) => !visibleKeys.has(r.rowKey)),
];
payloadRows = reorderedVisible.filter(isPrgrsSttsRowEditable);
return merged;
});
return payloadRows;
},
[isEditingLocked],
);
return {
rows,
visibleRows,
editingRowKey,
isEditingLocked,
addRow,
startEdit,
updateRow,
setDcarbYn,
cancelEdit,
commitEdit,
markDeleted,
reorderRows,
};
}
@@ -6,14 +6,14 @@ export const ELAPR_APLN_SAVE_MODE = {
DELETE: "D",
} as const;
export type ElaprAplnSaveMode =
(typeof ELAPR_APLN_SAVE_MODE)[keyof typeof ELAPR_APLN_SAVE_MODE];
export type ElaprAplnSaveMode = (typeof ELAPR_APLN_SAVE_MODE)[keyof typeof ELAPR_APLN_SAVE_MODE];
export interface ElaprApln {
aplnId: string;
elaprTmpltId: string;
aprvrUuid?: string;
aprvrNm?: string;
aprvrUserId?: string;
atrzSeComCd?: string;
atrzSeComNm?: string;
atrzSeqNo?: number;
@@ -1,4 +1,4 @@
import type { ElaprApln } from "@/entities/elapr";
import type { ElaprApln } from "./elapr-apln-interface";
/** 그리드 로컬 행 — API 응답 + UI 보조 필드 */
export interface ElaprAplnRow extends Omit<ElaprApln, "aplnId"> {
@@ -1,22 +1,47 @@
export interface ElaprDmndSaveRequest {
import type { ElaprDocPrgrsClientResponseDto, PageableParams, PageResponse } from "@/shared/model";
import type { ElaprPrgrsSttsSaveRequestDto } from "./elapr-prgrs-stts-interface";
/** 백엔드 ElaprDmndSaveRequestDto */
export interface ElaprDmndSaveRequestDto {
elaprDocNo: string;
elaprTmpltId: string;
dmndTtl: string;
dmndCn: string;
elaprPrgrsSttsList?: ElaprPrgrsSttsSaveRequestDto[];
}
export interface ElaprDmndResponse {
/** 백엔드 ElaprDmndResponseDto */
export interface ElaprDmndResponseDto {
elaprDmndId: string;
rqstrUuid?: string;
elaprDocNo: string;
elaprTmpltId: string;
dmndTtl: string;
dmndCn: string;
atrzSttsComCd: string;
rqstrUuid: string;
regDt: string;
updtDt: string;
atrzSttsComNm?: string;
dmndDt?: string;
updtDt?: string;
}
export interface ElaprDmndService {
create(body: ElaprDmndSaveRequest): Promise<ElaprDmndResponse>;
/** 백엔드 ElaprDmndSearchDto + Pageable */
export interface ElaprDmndSearchDto extends PageableParams {
rqstrUuid?: string;
atrzSttsComCd?: string;
dmndTtl?: string;
dmndDtStart?: string;
dmndDtEnd?: string;
}
/** 백엔드 ElaprDocPrgrsResponseDto */
export type ElaprDocPrgrsResponseDto = ElaprDocPrgrsClientResponseDto;
/** Controller public 메서드명·시그니처와 동일 */
export interface ElaprDmndService {
findPage(params: ElaprDmndSearchDto): Promise<PageResponse<ElaprDmndResponseDto>>;
findById(elaprDmndId: string): Promise<ElaprDmndResponseDto>;
save(body: ElaprDmndSaveRequestDto): Promise<ElaprDmndResponseDto>;
update(elaprDmndId: string, body: ElaprDmndSaveRequestDto): Promise<ElaprDmndResponseDto>;
updateStatus(elaprDmndId: string, atrzSttsComCd: string): Promise<ElaprDmndResponseDto>;
remove(elaprDmndId: string): Promise<void>;
findDocPrgrsByDocNo(elaprDocNo: string): Promise<ElaprDocPrgrsResponseDto[]>;
}
@@ -0,0 +1,92 @@
import type { PageableParams, PageResponse } from "@/shared/model";
export const ELAPR_PRGRS_STTS_SAVE_MODE = {
CREATE: "C",
READ: "R",
UPDATE: "U",
DELETE: "D",
} as const;
export type ElaprPrgrsSttsSaveMode =
(typeof ELAPR_PRGRS_STTS_SAVE_MODE)[keyof typeof ELAPR_PRGRS_STTS_SAVE_MODE];
/** 백엔드 ElaprPrgrsSttsResponseDto */
export interface ElaprPrgrsSttsResponseDto {
elaprPrgrsId: string;
elaprDmndId?: string;
aprvrUuid?: string;
atrzSeComCd?: string;
atrzSeComNm?: string;
atrzSeqNo?: number;
atrzPrgrsSttsComCd?: string;
atrzPrgrsSttsComNm?: string;
rjctRsn?: string;
atrzDt?: string;
dcarbYn?: string;
}
/** 백엔드 ElaprPrgrsSttsSaveRequestDto */
export interface ElaprPrgrsSttsSaveRequestDto {
mode?: ElaprPrgrsSttsSaveMode;
elaprPrgrsId?: string;
elaprDmndId?: string;
aprvrUuid?: string;
atrzSeComCd?: string;
atrzSeqNo?: number;
atrzPrgrsSttsComCd?: string;
dcarbYn?: string;
}
/** 백엔드 ElaprPrgrsSttsSearchDto */
export interface ElaprPrgrsSttsSearchDto {
atrzSeComCd: string;
atrzSeqNo: number;
atrzPrgrsSttsComCd?: string;
}
/** 백엔드 ElaprPrgrsSttsSearchDto + Pageable */
export interface ElaprPrgrsSttsMyDocSearchParams extends ElaprPrgrsSttsSearchDto, PageableParams {}
/** 백엔드 ElaprProcessRequestDto */
export interface ElaprProcessRequestDto {
atrzPrgrsSttsComCd: string;
rjctRsn?: string;
finalApprovalYn?: boolean;
}
/** 백엔드 ElaprApproverChangeRequestDto */
export interface ElaprApproverChangeRequestDto {
aprvrUuid: string;
}
/** 백엔드 ElaprApproverInsertRequestDto */
export interface ElaprApproverInsertRequestDto {
aprvrUuid: string;
atrzSeComCd: string;
atrzSeqNo: number;
}
/** Controller public 메서드명·시그니처와 동일 */
export interface ElaprPrgrsSttsService {
findAllByDmndId(elaprDmndId?: string): Promise<ElaprPrgrsSttsResponseDto[]>;
findById(elaprPrgrsId: string): Promise<ElaprPrgrsSttsResponseDto>;
save(requestDtos: ElaprPrgrsSttsSaveRequestDto[]): Promise<ElaprPrgrsSttsResponseDto[]>;
process(elaprPrgrsId: string, body: ElaprProcessRequestDto): Promise<ElaprPrgrsSttsResponseDto>;
processByDocNoAndAprvrUuid(
elaprDocNo: string,
aprvrUuid: string,
body: ElaprProcessRequestDto,
): Promise<ElaprPrgrsSttsResponseDto>;
delete(elaprPrgrsId: string): Promise<void>;
updateApprover(
elaprPrgrsId: string,
body: ElaprApproverChangeRequestDto,
): Promise<ElaprPrgrsSttsResponseDto>;
insertApprover(
elaprDmndId: string,
body: ElaprApproverInsertRequestDto,
): Promise<ElaprPrgrsSttsResponseDto>;
findMyElaperDocList(
params: ElaprPrgrsSttsMyDocSearchParams,
): Promise<PageResponse<ElaprPrgrsSttsResponseDto>>;
}
@@ -0,0 +1,5 @@
export interface ReviewerOption {
uuid: string;
name: string;
deptNm: string;
}
@@ -0,0 +1,125 @@
"use client";
import { RvwMnpwMngResponseDto, useRvwMnpwMngPage } from "@/entities/rvw-mnpw";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Skeleton,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@common/components";
import { type ReviewerOption } from "../model/reviewer-option";
const PAGE_SIZE = 500;
interface AprvrPickerDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
atrzSeComCd: string;
onSelect: (reviewer: ReviewerOption) => void;
}
function AprvrPickerDialog({ open, onOpenChange, atrzSeComCd, onSelect }: AprvrPickerDialogProps) {
const { data, isPending, isError } = useRvwMnpwMngPage(
{
atrzSeComCd,
page: 0,
size: PAGE_SIZE,
sort: "userUuidNm,asc",
},
{ enabled: open },
);
const items = data?.content ?? [];
function fromRvwMnpwMng(item: RvwMnpwMngResponseDto): ReviewerOption {
return {
uuid: item.userUuid,
name: item.userUuidNm ?? "",
deptNm: item.ognzNm ?? "",
};
}
function handleSelect(item: (typeof items)[number]) {
onSelect(fromRvwMnpwMng(item));
onOpenChange(false);
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[56rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>검토인력 선택</DialogTitle>
</DialogHeader>
<div className="h-[36rem] overflow-auto">
{isPending ? (
<div className="flex flex-col gap-2">
<Skeleton className="h-[4rem]" />
<Skeleton className="h-[16rem]" />
</div>
) : isError ? (
<p className="py-8 text-center text-lg text-muted-foreground">
검토인력 목록을 불러오지 못했습니다.
</p>
) : items.length === 0 ? (
<p className="py-8 text-center text-lg text-muted-foreground">
배치된 검토인력이 없습니다.
</p>
) : (
<Table className="w-full">
<colgroup>
<col width="25%" />
<col width="25%" />
<col width="15%" />
<col width="15%" />
<col width="20%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead>성명</TableHead>
<TableHead>부서</TableHead>
<TableHead>직위</TableHead>
<TableHead>직급</TableHead>
<TableHead>선택</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item) => (
<TableRow key={`${item.atrzSeComCd}-${item.ognzSn}-${item.userUuid}`}>
<TableCell align="center">{item.userUuidNm || item.userUuid}</TableCell>
<TableCell align="center">{item.ognzNm || "-"}</TableCell>
<TableCell align="center">{item.jbpsSeNm || "-"}</TableCell>
<TableCell align="center">{item.jbgdSeNm || "-"}</TableCell>
<TableCell align="center">
<Button type="button" size="sm" onClick={() => handleSelect(item)}>
선택
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
<DialogFooter>
<Button type="button" color="white" onClick={() => onOpenChange(false)}>
닫기
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export { AprvrPickerDialog };
@@ -0,0 +1,202 @@
"use client";
import { useEffect, useState } from "react";
import { closestCenter, DndContext, type DragEndEvent } from "@dnd-kit/core";
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { useAlertStore } from "@/shared/lib";
import {
Button,
Skeleton,
Table,
TableBody,
TableHead,
TableHeader,
TableRow,
} from "@common/components";
import { useElaprAplnPage } from "../api/queries";
import { validateAplnRow } from "../lib/apln-validators";
import { useAplnRows } from "../lib/use-apln-rows";
import { useDndSensors } from "../lib/use-dnd-sensors";
import type { ElaprAplnRow } from "../model/elapr-apln-row-types";
import type { ReviewerOption } from "../model/reviewer-option";
import { AprvrPickerDialog } from "./aprvr-picker-dialog";
import { SortableAplnPickerRow } from "./sortable-apln-picker-row";
interface ElaprAplnPickerGridProps {
elaprTmpltId: string | null;
onRowsChange?: (rows: ElaprAplnRow[]) => void;
onEditingLockedChange?: (locked: boolean) => void;
}
function ElaprAplnPickerGrid({
elaprTmpltId,
onRowsChange,
onEditingLockedChange,
}: ElaprAplnPickerGridProps) {
const [pickerOpen, setPickerOpen] = useState(false);
const [pickingRowKey, setPickingRowKey] = useState<string | null>(null);
const alert = useAlertStore((state) => state.setMessage);
const aplnQuery = useElaprAplnPage({
elaprTmpltId: elaprTmpltId ?? undefined,
page: 0,
size: 200,
sort: "atrzSeqNo,asc",
});
const {
rows,
visibleRows,
editingRowKey,
isEditingLocked,
addRow,
startEdit,
updateRow,
cancelEdit,
commitEdit,
markDeleted,
reorderRows,
} = useAplnRows({
elaprTmpltId,
serverRows: aplnQuery.data?.content,
});
useEffect(() => {
onRowsChange?.(rows);
}, [rows, onRowsChange]);
useEffect(() => {
onEditingLockedChange?.(isEditingLocked);
}, [isEditingLocked, onEditingLockedChange]);
const sensors = useDndSensors();
const isDisabled = !elaprTmpltId;
const pickingRow = pickingRowKey ? rows.find((r) => r.rowKey === pickingRowKey) : undefined;
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (!over || active.id === over.id) return;
reorderRows(String(active.id), String(over.id));
}
function handleRowChange(row: ElaprAplnRow, patch: Partial<ElaprAplnRow>) {
updateRow(row.rowKey, patch);
}
function handleRowSave(row: ElaprAplnRow) {
const fieldError = validateAplnRow(row);
if (fieldError) {
alert(fieldError);
return;
}
commitEdit(row.rowKey);
}
function handleRowDelete(row: ElaprAplnRow) {
markDeleted(row.rowKey);
}
function handleReviewerSelect(reviewer: ReviewerOption) {
if (!pickingRowKey) return;
updateRow(pickingRowKey, {
aprvrUuid: reviewer.uuid,
aprvrNm: reviewer.name,
});
setPickingRowKey(null);
}
return (
<section className="card card-border mx-0 min-h-[32rem] flex-1 pt-0">
<div className="card-header flex flex-wrap items-center justify-between gap-3">
<h3 className="text-2xl font-semibold">결재선 관리</h3>
<div className="flex flex-wrap items-center gap-3">
<Button
size="sm"
type="button"
color="green"
disabled={isDisabled || isEditingLocked}
onClick={addRow}
>
결재선 추가
</Button>
</div>
</div>
{aplnQuery.isPending && elaprTmpltId ? (
<div className="flex flex-col gap-2">
<Skeleton className="h-[4rem]" />
<Skeleton className="h-[16rem]" />
</div>
) : !elaprTmpltId ? (
<p className="py-8 text-center text-lg text-muted-foreground">결재서식을 선택하세요.</p>
) : visibleRows.length === 0 ? (
<p className="py-8 text-center text-lg text-muted-foreground">
등록된 결재선이 없습니다. [결재선 추가]로 결재자를 등록하세요.
</p>
) : (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<Table className="w-full">
<colgroup>
<col width="10%" />
<col width="10%" />
<col width="10%" />
<col width="50%" />
<col width="20%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead className="w-10" />
<TableHead>결재순서</TableHead>
<TableHead>결재구분</TableHead>
<TableHead>결재자</TableHead>
<TableHead>관리</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<SortableContext
items={visibleRows.map((r) => r.rowKey)}
strategy={verticalListSortingStrategy}
>
{visibleRows.map((row, index) => (
<SortableAplnPickerRow
key={row.rowKey}
row={row}
displaySeqNo={index + 1}
isEditing={editingRowKey === row.rowKey}
isLocked={isEditingLocked}
dragDisabled={isEditingLocked || !row.atrzSeComCd}
onStartEdit={() => startEdit(row.rowKey)}
onSave={() => handleRowSave(row)}
onCancel={cancelEdit}
onDelete={() => handleRowDelete(row)}
onChange={(patch) => handleRowChange(row, patch)}
onPickAprvr={() => {
setPickingRowKey(row.rowKey);
setPickerOpen(true);
}}
/>
))}
</SortableContext>
</TableBody>
</Table>
</DndContext>
)}
<AprvrPickerDialog
open={pickerOpen}
onOpenChange={setPickerOpen}
atrzSeComCd={pickingRow?.atrzSeComCd ?? ""}
onSelect={handleReviewerSelect}
/>
</section>
);
}
export { ElaprAplnPickerGrid };
@@ -0,0 +1,408 @@
"use client";
import { useState } from "react";
import { closestCenter, DndContext, type DragEndEvent } from "@dnd-kit/core";
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { useQueryClient } from "@tanstack/react-query";
import { AxiosError } from "axios";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore } from "@/shared/lib";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Skeleton,
Table,
TableBody,
TableHead,
TableHeader,
TableRow,
Textarea,
} from "@common/components";
import { elaprPrgrsSttsService } from "../api/elapr-prgrs-stts-service";
import { useElaprDocPrgrsByDocNo, useSaveElaprPrgrsStts } from "../api/queries";
import { validateAtrzOrder } from "../lib/apln-validators";
import { findCurrentUserPendingApprovalRequest } from "../lib/approval-request-utils";
import {
buildPrgrsSttsSavePayload,
hasUnsavedPrgrsSttsChanges,
isPrgrsSttsRowEditable,
validateAllPrgrsSttsRows,
validatePrgrsSttsRow,
} from "../lib/prgrs-stts-save-utils";
import { useDndSensors } from "../lib/use-dnd-sensors";
import { usePrgrsSttsRows } from "../lib/use-prgrs-stts-rows";
import type { ReviewerOption } from "../model/reviewer-option";
import { AprvrPickerDialog } from "./aprvr-picker-dialog";
import { SortablePrgrsSttsRow } from "./sortable-prgrs-stts-row";
interface ElaprApprovalDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
elaprDocNo: string;
currentUserUuid: string;
onSuccess: () => void;
}
const UNSAVED_PRGRS_STTS_MESSAGE = "결재선 변경 이력이 있습니다.\n결재선 저장을 해 주세요.";
function ElaprApprovalDialog({
open,
onOpenChange,
elaprDocNo,
currentUserUuid,
onSuccess,
}: ElaprApprovalDialogProps) {
const queryClient = useQueryClient();
const alert = useAlertStore((state) => state.setMessage);
const [rjctRsn, setRjctRsn] = useState("");
const [isProcessing, setIsProcessing] = useState(false);
const [pickerOpen, setPickerOpen] = useState(false);
const [pickingRowKey, setPickingRowKey] = useState<string | null>(null);
const { data: elaprDocPrgrsList, isLoading } = useElaprDocPrgrsByDocNo(open ? elaprDocNo : null);
const elaprDmndId = elaprDocPrgrsList?.[0]?.elaprDmndId;
const {
rows,
visibleRows,
editingRowKey,
isEditingLocked,
addRow,
startEdit,
updateRow,
setDcarbYn,
cancelEdit,
commitEdit,
markDeleted,
reorderRows,
} = usePrgrsSttsRows({
serverRows: elaprDocPrgrsList,
elaprDmndId,
});
const savePrgrsSttsMutation = useSaveElaprPrgrsStts();
const sensors = useDndSensors();
const currentUserPrgrs = findCurrentUserPendingApprovalRequest(
elaprDocPrgrsList,
currentUserUuid,
);
const canApprove = Boolean(currentUserPrgrs?.elaprPrgrsId);
const pickingRow = pickingRowKey ? rows.find((r) => r.rowKey === pickingRowKey) : undefined;
const isBusy = isProcessing || savePrgrsSttsMutation.isPending;
const isPrgrsSttsUnsaved = hasUnsavedPrgrsSttsChanges(rows, elaprDocPrgrsList ?? []);
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (!over || active.id === over.id) return;
reorderRows(String(active.id), String(over.id));
}
function handleRowSave(rowKey: string) {
const row = rows.find((r) => r.rowKey === rowKey);
if (!row) return;
const fieldError = validatePrgrsSttsRow(row);
if (fieldError) {
alert(fieldError);
return;
}
commitEdit(rowKey);
}
function handleReviewerSelect(reviewer: ReviewerOption) {
if (!pickingRowKey) return;
updateRow(pickingRowKey, {
aprvrUuid: reviewer.uuid,
aprvrNm: reviewer.name,
ognzNm: reviewer.deptNm,
});
setPickingRowKey(null);
}
function assertPrgrsSttsSaved(): boolean {
if (isEditingLocked) {
alert(UNSAVED_PRGRS_STTS_MESSAGE);
return false;
}
if (isPrgrsSttsUnsaved) {
alert(UNSAVED_PRGRS_STTS_MESSAGE);
return false;
}
return true;
}
async function handleSavePrgrsStts() {
if (!elaprDmndId) {
alert("결재 요청 정보가 없습니다.");
return;
}
const validationError = validateAllPrgrsSttsRows(rows, isEditingLocked);
if (validationError) {
alert(validationError);
return;
}
const orderError = validateAtrzOrder(rows);
if (orderError) {
alert(orderError);
return;
}
const payload = buildPrgrsSttsSavePayload(rows, elaprDmndId, elaprDocPrgrsList ?? []);
if (payload.length === 0) {
alert("변경된 결재선이 없습니다.");
return;
}
setIsProcessing(true);
try {
await savePrgrsSttsMutation.mutateAsync(payload);
alert("결재선이 저장되었습니다.");
await queryClient.invalidateQueries({
queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", elaprDocNo],
});
onSuccess();
} catch (error: unknown) {
let errorMessage = "결재선 저장 중 오류가 발생했습니다.";
if (error instanceof AxiosError) {
errorMessage = error.response?.data?.message ?? errorMessage;
}
alert(errorMessage);
} finally {
setIsProcessing(false);
}
}
async function handleApprove() {
if (!currentUserPrgrs?.elaprPrgrsId) return;
if (!assertPrgrsSttsSaved()) return;
setIsProcessing(true);
try {
await elaprPrgrsSttsService.process(currentUserPrgrs.elaprPrgrsId, {
atrzPrgrsSttsComCd: "ATRZ_PRGRS_003",
});
alert("결재가 완료되었습니다.");
queryClient.invalidateQueries({
queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", elaprDocNo],
});
onSuccess();
onOpenChange(false);
} catch (error: unknown) {
let errorMessage = "";
if (error instanceof AxiosError) {
errorMessage = error.response?.data?.message ?? "";
} else {
errorMessage = "결재 처리 중 오류가 발생했습니다.";
}
alert(errorMessage);
} finally {
setIsProcessing(false);
}
}
async function handleReject() {
if (!currentUserPrgrs?.elaprPrgrsId) return;
if (!rjctRsn.trim()) {
alert("반려 사유를 입력해주세요.");
return;
}
if (!assertPrgrsSttsSaved()) return;
setIsProcessing(true);
try {
await elaprPrgrsSttsService.process(currentUserPrgrs.elaprPrgrsId, {
atrzPrgrsSttsComCd: "ATRZ_PRGRS_004",
rjctRsn: rjctRsn.trim(),
});
alert("반려 처리가 완료되었습니다.");
queryClient.invalidateQueries({
queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", elaprDocNo],
});
onSuccess();
onOpenChange(false);
} catch (error: unknown) {
let errorMessage = "";
if (error instanceof AxiosError) {
errorMessage = error.response?.data?.message ?? "";
} else {
errorMessage = "반려 처리 중 오류가 발생했습니다.";
}
alert(errorMessage);
} finally {
setIsProcessing(false);
}
}
function handleClose() {
setRjctRsn("");
setPickingRowKey(null);
onOpenChange(false);
}
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-[72rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>전자결재</DialogTitle>
</DialogHeader>
<div className="space-y-6">
<div>
<div className="mb-3 flex flex-wrap items-center justify-between gap-3">
<h4 className="text-xl font-semibold">결재 진행 단계</h4>
<div className="flex items-center gap-2">
<Button
size="sm"
type="button"
color="white"
disabled={!elaprDmndId || isEditingLocked || isBusy}
onClick={addRow}
>
결재선 추가
</Button>
{elaprDmndId && (
<Button
size="sm"
type="button"
onClick={handleSavePrgrsStts}
disabled={!isPrgrsSttsUnsaved || isEditingLocked || isBusy}
>
결재선 저장
</Button>
)}
</div>
</div>
{isLoading ? (
<div className="space-y-3">
<Skeleton className="h-12 w-full" />
<Skeleton className="h-32 w-full" />
</div>
) : visibleRows.length === 0 ? (
<div className="py-8 text-center text-gray-500">
{elaprDmndId
? "등록된 결재선이 없습니다. [결재선 추가]로 결재자를 등록하세요."
: "결재 정보가 없습니다."}
</div>
) : (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<Table className="w-full">
<colgroup>
<col width="8%" />
<col width="8%" />
<col width="10%" />
<col width="30%" />
<col width="10%" />
<col width="10%" />
<col width="24%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead className="w-10" />
<TableHead>결재순서</TableHead>
<TableHead>결재구분</TableHead>
<TableHead>결재자</TableHead>
<TableHead>전결자 지정</TableHead>
<TableHead>결재상태</TableHead>
<TableHead>관리</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<SortableContext
items={visibleRows.map((r) => r.rowKey)}
strategy={verticalListSortingStrategy}
>
{visibleRows.map((row, index) => (
<SortablePrgrsSttsRow
key={row.rowKey}
row={row}
displaySeqNo={index + 1}
isEditing={editingRowKey === row.rowKey}
isLocked={isEditingLocked}
dragDisabled={
isEditingLocked || !isPrgrsSttsRowEditable(row) || !row.atrzSeComCd
}
onStartEdit={() => startEdit(row.rowKey)}
onSave={() => handleRowSave(row.rowKey)}
onCancel={cancelEdit}
onDelete={() => markDeleted(row.rowKey)}
onChange={(patch) => updateRow(row.rowKey, patch)}
onPickAprvr={() => {
setPickingRowKey(row.rowKey);
setPickerOpen(true);
}}
onToggleDcarb={(checked) => setDcarbYn(row.rowKey, checked)}
/>
))}
</SortableContext>
</TableBody>
</Table>
</DndContext>
)}
</div>
{canApprove && (
<div>
<h4 className="mb-3 text-xl font-semibold">반려 의견</h4>
<Textarea
value={rjctRsn}
onChange={(event) => setRjctRsn(event.target.value)}
placeholder="반려 시 사유를 입력하세요"
rows={4}
className="!text-xl"
/>
</div>
)}
</div>
<DialogFooter>
<div className="flex w-full items-center justify-between">
<Button type="button" color="white" onClick={handleClose} disabled={isBusy}>
취소
</Button>
{canApprove && (
<div className="flex items-center gap-2">
<Button onClick={handleApprove} disabled={isBusy}>
결재
</Button>
<Button color="red" onClick={handleReject} disabled={isBusy}>
반려
</Button>
</div>
)}
</div>
</DialogFooter>
</DialogContent>
<AprvrPickerDialog
open={pickerOpen}
onOpenChange={setPickerOpen}
atrzSeComCd={pickingRow?.atrzSeComCd ?? ""}
onSelect={handleReviewerSelect}
/>
</Dialog>
);
}
export { ElaprApprovalDialog };
@@ -0,0 +1,95 @@
"use client";
import { Fragment } from "react";
import { ChevronRight } from "lucide-react";
import type { ElaprDocPrgrsClientResponseDto } from "@/shared/model";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
import { cn, Skeleton } from "@common/components";
import { useElaprDocPrgrsByDocNo } from "../api/queries";
import { getApproverStatus, sortByAtrzSeqNo } from "../lib/approval-utils";
const REJECT_STATUS_CD = "ATRZ_PRGRS_004";
interface ElaprDocPrgrsStatusProps {
elaprDocNo: string | null | undefined;
}
function ElaprDocPrgrsStep({ item }: { item: ElaprDocPrgrsClientResponseDto }) {
const status = getApproverStatus(item.atrzPrgrsSttsComCd);
const isRejected = item.atrzPrgrsSttsComCd === REJECT_STATUS_CD;
const rjctRsn = item.rjctRsn?.trim();
const metaParts = [item.ognzNm, item.jbpsSeNm].filter(Boolean);
const statusBadge = (
<span
className={cn(
"inline-flex items-center rounded-sm px-2 py-1 text-lg font-medium",
status.badgeClass,
isRejected && rjctRsn && "cursor-help underline decoration-dotted underline-offset-4",
)}
>
{status.label}
</span>
);
return (
<div className="flex min-w-[12rem] flex-1 flex-col items-center gap-3 p-4 text-center">
{isRejected && rjctRsn ? (
<Tooltip>
<TooltipTrigger asChild>{statusBadge}</TooltipTrigger>
<TooltipContent sideOffset={4} className="max-w-md whitespace-pre-wrap">
{rjctRsn}
</TooltipContent>
</Tooltip>
) : (
statusBadge
)}
<div className="flex flex-col items-center gap-1">
<div className="flex items-center gap-2">
<span className="text-xl text-muted-foreground">[{item.atrzSeComNm || "-"}]</span>
<span className="text-xl font-semibold">{item.aprvrNm || item.aprvrUuid || "-"}</span>
</div>
{metaParts.length > 0 && (
<span className="text-base text-gray-500">{metaParts.join(" · ")}</span>
)}
</div>
</div>
);
}
function ElaprDocPrgrsStatus({ elaprDocNo }: ElaprDocPrgrsStatusProps) {
const { data, isLoading } = useElaprDocPrgrsByDocNo(elaprDocNo);
const steps = sortByAtrzSeqNo(data ?? []);
if (isLoading) {
return (
<div className="flex items-center gap-3 px-4 py-6">
<Skeleton className="h-24 flex-1 rounded-sm" />
<Skeleton className="h-24 flex-1 rounded-sm" />
<Skeleton className="h-24 flex-1 rounded-sm" />
</div>
);
}
if (steps.length === 0) {
return <div className="py-8 text-center text-xl text-gray-500">결재 정보가 없습니다.</div>;
}
return (
<div className="flex flex-wrap items-stretch justify-start gap-4 overflow-x-auto">
{steps.map((item, index) => (
<Fragment key={item.elaprPrgrsId ?? `${item.atrzSeqNo}-${item.aprvrUuid ?? index}`}>
<div className="flex items-center">
{index > 0 && <ChevronRight className="size-8 text-muted-foreground" />}
</div>
<ElaprDocPrgrsStep item={item} />
</Fragment>
))}
</div>
);
}
export { ElaprDocPrgrsStatus };
@@ -0,0 +1,166 @@
"use client";
import { useCallback, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@common/components";
import { useElaprTmpltPage } from "../api/queries";
import {
getVisibleRows,
validateAllRowsForSave,
validateAtrzOrder,
validateUniqueSeqNo,
} from "../lib/apln-validators";
import type { ElaprAplnRow } from "../model/elapr-apln-row-types";
import type { ElaprTmplt } from "../model/elapr-tmplt-interface";
import { ElaprAplnPickerGrid } from "./elapr-apln-picker-grid";
import { ElaprTmpltSelectTable } from "./elapr-tmplt-select-table";
const PAGE_SIZE = 100;
interface ElaprTmpltPickerConfirmPayload {
tmplt: ElaprTmplt;
aplnRows: ElaprAplnRow[];
}
interface ElaprTmpltPickerDialogProps {
open: boolean;
onClose: () => void;
docSeComCd: string;
onSelect?: (tmplt: ElaprTmplt) => void;
onConfirm?: (payload: ElaprTmpltPickerConfirmPayload) => void;
}
function ElaprTmpltPickerDialog({
open,
onClose,
docSeComCd,
onSelect,
onConfirm,
}: ElaprTmpltPickerDialogProps) {
const alert = useAlertStore((state) => state.setMessage);
const [selectedTmplt, setSelectedTmplt] = useState<ElaprTmplt | null>(null);
const [aplnRows, setAplnRows] = useState<ElaprAplnRow[]>([]);
const [isEditingLocked, setIsEditingLocked] = useState(false);
const tmpltQuery = useElaprTmpltPage({
page: 0,
size: PAGE_SIZE,
sort: "regDt,desc",
docSeComCd,
});
const handleRowsChange = useCallback((rows: ElaprAplnRow[]) => {
setAplnRows(rows);
}, []);
const handleEditingLockedChange = useCallback((locked: boolean) => {
setIsEditingLocked(locked);
}, []);
function handleSelectTmplt(tmplt: ElaprTmplt | null) {
setSelectedTmplt(tmplt);
setAplnRows([]);
setIsEditingLocked(false);
}
function handleOpenChange(isOpen: boolean) {
if (!isOpen) {
setSelectedTmplt(null);
setAplnRows([]);
setIsEditingLocked(false);
onClose();
}
}
function handleConfirm() {
if (!selectedTmplt) {
alert("결재서식을 선택해 주세요.");
return;
}
const rowsError = validateAllRowsForSave(aplnRows, isEditingLocked);
if (rowsError) {
alert(rowsError);
return;
}
const orderError = validateAtrzOrder(aplnRows);
if (orderError) {
alert(orderError);
return;
}
const seqError = validateUniqueSeqNo(aplnRows);
if (seqError) {
alert(seqError);
return;
}
const visibleRows = getVisibleRows(aplnRows);
const payload: ElaprTmpltPickerConfirmPayload = {
tmplt: selectedTmplt,
aplnRows: visibleRows,
};
if (onConfirm) {
onConfirm(payload);
} else {
onSelect?.(selectedTmplt);
}
setSelectedTmplt(null);
setAplnRows([]);
setIsEditingLocked(false);
onClose();
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-[120rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>결재선 선택</DialogTitle>
</DialogHeader>
<div className="flex gap-6">
<ElaprTmpltSelectTable
query={tmpltQuery}
selectedTmpltId={selectedTmplt?.elaprTmpltId ?? null}
onSelectTmplt={handleSelectTmplt}
/>
<ElaprAplnPickerGrid
elaprTmpltId={selectedTmplt?.elaprTmpltId ?? null}
onRowsChange={handleRowsChange}
onEditingLockedChange={handleEditingLockedChange}
/>
</div>
<DialogFooter>
<Button type="button" color="white" onClick={() => handleOpenChange(false)}>
닫기
</Button>
<Button
type="button"
disabled={!selectedTmplt || isEditingLocked}
onClick={handleConfirm}
>
확인
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export { ElaprTmpltPickerDialog };
export type { ElaprTmpltPickerConfirmPayload };
@@ -0,0 +1,97 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import type { UseQueryResult } from "@tanstack/react-query";
import { createColumnHelper, getCoreRowModel, type RowSelectionState } from "@tanstack/react-table";
import { useDataTable } from "@/shared/lib";
import type { PageResponse } from "@/shared/model";
import { Skeleton } from "@common/components";
import type { ElaprTmplt } from "../model/elapr-tmplt-interface";
const columnHelper = createColumnHelper<ElaprTmplt>();
interface ElaprTmpltSelectTableProps {
query: UseQueryResult<PageResponse<ElaprTmplt>>;
selectedTmpltId: string | null;
onSelectTmplt: (tmplt: ElaprTmplt | null) => void;
}
function ElaprTmpltSelectTable({
query,
selectedTmpltId,
onSelectTmplt,
}: ElaprTmpltSelectTableProps) {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
useEffect(() => {
if (selectedTmpltId) {
setRowSelection({ [selectedTmpltId]: true });
} else {
setRowSelection({});
}
}, [selectedTmpltId]);
const columns = useMemo(
() => [
columnHelper.accessor("elaprTmpltNm", {
header: "결재서식명",
meta: { align: "left" },
}),
],
[],
);
const { tableComponent } = useDataTable({
query,
tableOptions: {
columns,
state: { rowSelection },
enableRowSelection: true,
enableMultiRowSelection: false,
getRowId: (row) => row.elaprTmpltId,
onRowSelectionChange: (updater) => {
const next = typeof updater === "function" ? updater(rowSelection) : updater;
setRowSelection(next);
const selectedId = Object.keys(next)[0];
if (!selectedId) {
onSelectTmplt(null);
return;
}
const selected = query.data?.content.find((t) => t.elaprTmpltId === selectedId);
onSelectTmplt(selected ?? null);
},
getCoreRowModel: getCoreRowModel(),
},
dataTableOptions: {
onRowClick: (row) => {
setRowSelection({ [row.elaprTmpltId]: true });
onSelectTmplt(row);
},
},
});
return (
<section className="card card-border mx-0 min-h-[32rem] flex-[0.5] bg-background pt-0">
<div className="card-header h-[4.6rem]">
<h3 className="text-2xl font-semibold">전자결재서식</h3>
</div>
{query.isPending && (
<div className="flex flex-col gap-2 p-4">
<Skeleton className="h-[4rem]" />
<Skeleton className="h-[16rem]" />
</div>
)}
{query.isError && (
<p className="py-8 text-center text-lg text-muted-foreground">
결재서식 목록을 불러오지 못했습니다.
</p>
)}
{!query.isPending && !query.isError && tableComponent}
</section>
);
}
export { ElaprTmpltSelectTable };
@@ -0,0 +1,153 @@
"use client";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { MoveVertical } from "lucide-react";
import { findCodeLabel, useCodeList } from "@/entities/code";
import {
Button,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
TableCell,
TableRow,
} from "@common/components";
import type { ElaprAplnRow } from "../model/elapr-apln-row-types";
interface SortableAplnPickerRowProps {
row: ElaprAplnRow;
displaySeqNo: number;
isEditing: boolean;
isLocked: boolean;
dragDisabled: boolean;
onStartEdit: () => void;
onSave: () => void;
onCancel: () => void;
onDelete: () => void;
onChange: (patch: Partial<ElaprAplnRow>) => void;
onPickAprvr: () => void;
}
function SortableAplnPickerRow({
row,
displaySeqNo,
isEditing,
isLocked,
dragDisabled,
onStartEdit,
onSave,
onCancel,
onDelete,
onChange,
onPickAprvr,
}: SortableAplnPickerRowProps) {
const { data: atrzSeCodes } = useCodeList("ATRZSE");
const selectableAtrzSeCodes = atrzSeCodes?.filter((code) => code.lwpoCd !== "ATRZ01");
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: row.rowKey,
disabled: dragDisabled,
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.6 : 1,
};
const rowDisabled = isLocked && !isEditing;
const atrzSeLabel = row.atrzSeComNm || findCodeLabel(atrzSeCodes, row.atrzSeComCd);
return (
<TableRow ref={setNodeRef} style={style} className={isEditing ? "bg-accent/30" : undefined}>
<TableCell align="center" className="w-10">
<Button
type="button"
size="icon"
className="cursor-grab !bg-transparent text-muted-foreground disabled:cursor-not-allowed disabled:opacity-40"
disabled={dragDisabled}
{...attributes}
{...listeners}
aria-label="순서 변경"
>
<MoveVertical className="!size-6" />
</Button>
</TableCell>
<TableCell align="center">{displaySeqNo}</TableCell>
<TableCell align="center">
{isEditing ? (
<Select
value={row.atrzSeComCd ?? ""}
onValueChange={(v) =>
onChange({ atrzSeComCd: v, atrzSeComNm: findCodeLabel(selectableAtrzSeCodes, v) })
}
>
<SelectTrigger className="min-w-[120px]">
<SelectValue placeholder="결재구분" />
</SelectTrigger>
<SelectContent>
{selectableAtrzSeCodes?.map((code) => (
<SelectItem key={code.lwpoCd} value={code.lwpoCd}>
{code.lwpoCdNm}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
atrzSeLabel || "-"
)}
</TableCell>
<TableCell align="center">
<div className="flex items-center justify-center gap-2">
<span>{row.aprvrNm || row.aprvrUuid || ""}</span>
{isEditing && (
<Button type="button" size="sm" onClick={onPickAprvr} disabled={!row.atrzSeComCd}>
선택
</Button>
)}
</div>
</TableCell>
<TableCell align="center">
<div className="flex justify-center gap-2">
{isEditing ? (
<>
<Button type="button" size="sm" onClick={onSave}>
확인
</Button>
<Button type="button" size="sm" color="white" onClick={onCancel}>
취소
</Button>
</>
) : (
<>
<Button
type="button"
size="sm"
color="white"
disabled={rowDisabled}
onClick={onStartEdit}
>
수정
</Button>
<Button
type="button"
size="sm"
color="gray"
disabled={rowDisabled}
onClick={onDelete}
>
삭제
</Button>
</>
)}
</div>
</TableCell>
</TableRow>
);
}
export { SortableAplnPickerRow };
@@ -0,0 +1,212 @@
"use client";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { MoveVertical } from "lucide-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
import { findCodeLabel, useCodeList } from "@/entities/code";
import {
Button,
Checkbox,
cn,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
TableCell,
TableRow,
} from "@common/components";
import { getApproverStatus } from "../lib/approval-utils";
import {
type ElaprPrgrsSttsRow,
isPrgrsSttsRowDcarbable,
isPrgrsSttsRowEditable,
} from "../lib/prgrs-stts-save-utils";
interface SortablePrgrsSttsRowProps {
row: ElaprPrgrsSttsRow;
displaySeqNo: number;
isEditing: boolean;
isLocked: boolean;
dragDisabled: boolean;
onStartEdit: () => void;
onSave: () => void;
onCancel: () => void;
onDelete: () => void;
onChange: (patch: Partial<ElaprPrgrsSttsRow>) => void;
onPickAprvr: () => void;
onToggleDcarb: (checked: boolean) => void;
}
function SortablePrgrsSttsRow({
row,
displaySeqNo,
isEditing,
isLocked,
dragDisabled,
onStartEdit,
onSave,
onCancel,
onDelete,
onChange,
onPickAprvr,
onToggleDcarb,
}: SortablePrgrsSttsRowProps) {
const { data: atrzSeCodes } = useCodeList("ATRZSE");
const selectableAtrzSeCodes = atrzSeCodes?.filter((code) => code.lwpoCd !== "ATRZ01");
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: row.rowKey,
disabled: dragDisabled,
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.6 : 1,
};
const isEditable = isPrgrsSttsRowEditable(row);
const isDcarbable = isPrgrsSttsRowDcarbable(row);
const rowDisabled = isLocked && !isEditing;
const atrzSeLabel = row.atrzSeComNm || findCodeLabel(atrzSeCodes, row.atrzSeComCd);
const status = getApproverStatus(row.atrzPrgrsSttsComCd);
const isRejectedRow = row.atrzPrgrsSttsComCd === "ATRZ_PRGRS_004";
const rjctRsn = row.rjctRsn?.trim();
const statusBadge = (
<span
className={cn(
"inline-flex items-center rounded-sm px-3 py-1.5 text-lg font-semibold",
status.badgeClass,
isRejectedRow && rjctRsn && "cursor-help underline decoration-dotted underline-offset-4",
)}
>
{status.label}
</span>
);
return (
<TableRow ref={setNodeRef} style={style} className={isEditing ? "bg-accent/30" : undefined}>
<TableCell align="center" className="w-10">
{!dragDisabled && (
<Button
type="button"
size="icon"
className="cursor-grab !bg-transparent text-muted-foreground disabled:cursor-not-allowed disabled:opacity-40"
{...attributes}
{...listeners}
aria-label="순서 변경"
>
<MoveVertical className="!size-6" />
</Button>
)}
</TableCell>
<TableCell align="center">{displaySeqNo}</TableCell>
<TableCell align="center">
{isEditing ? (
<Select
value={row.atrzSeComCd ?? ""}
onValueChange={(v) =>
onChange({ atrzSeComCd: v, atrzSeComNm: findCodeLabel(selectableAtrzSeCodes, v) })
}
>
<SelectTrigger className="min-w-[120px]">
<SelectValue placeholder="결재구분" />
</SelectTrigger>
<SelectContent>
{selectableAtrzSeCodes?.map((code) => (
<SelectItem key={code.lwpoCd} value={code.lwpoCd}>
{code.lwpoCdNm}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
atrzSeLabel || "-"
)}
</TableCell>
<TableCell align="center">
<div className="flex flex-col items-center gap-1">
<div className="flex items-center justify-center gap-2">
<span>{row.aprvrNm || row.aprvrUuid || "-"}</span>
{isEditing && (
<Button type="button" size="sm" onClick={onPickAprvr}>
선택
</Button>
)}
</div>
{!isEditing && (row.ognzNm || row.jbpsSeNm || row.jbgdSeNm) && (
<div className="text-base text-gray-500">
{row.ognzNm ?? ""}
{row.jbpsSeNm ? ` · ${row.jbpsSeNm}` : ""}
</div>
)}
</div>
</TableCell>
<TableCell align="center">
{isDcarbable && !rowDisabled && (
<Checkbox
checked={row.dcarbYn === "Y"}
onCheckedChange={(checked) => onToggleDcarb(checked === true)}
aria-label="전결자 지정"
/>
)}
</TableCell>
<TableCell align="center">
{isRejectedRow && rjctRsn ? (
<Tooltip>
<TooltipTrigger asChild>{statusBadge}</TooltipTrigger>
<TooltipContent sideOffset={4} className="max-w-md whitespace-pre-wrap">
{rjctRsn}
</TooltipContent>
</Tooltip>
) : (
statusBadge
)}
</TableCell>
<TableCell align="center">
<div className="flex justify-center gap-2">
{isEditing ? (
<>
<Button type="button" size="sm" onClick={onSave}>
확인
</Button>
<Button type="button" size="sm" color="white" onClick={onCancel}>
취소
</Button>
</>
) : isEditable ? (
<>
<Button
type="button"
size="sm"
color="white"
disabled={rowDisabled}
onClick={onStartEdit}
>
수정
</Button>
<Button
type="button"
size="sm"
color="gray"
disabled={rowDisabled}
onClick={onDelete}
>
삭제
</Button>
</>
) : (
<span className="text-sm text-muted-foreground">-</span>
)}
</div>
</TableCell>
</TableRow>
);
}
export { SortablePrgrsSttsRow };
+10 -19
View File
@@ -11,50 +11,41 @@ import type {
EsntlAtchFileMngTreeSearchParams,
} from "../model/esntl-interface";
const BASE = ENDPOINT.AS_SERVICE.ESNTL_ATCH_FILE;
async function findTree(
params: EsntlAtchFileMngTreeSearchParams,
): Promise<EsntlAtchFileMngTreeResponseDto[]> {
const response = await client.get<EsntlAtchFileMngTreeResponseDto[]>(
`${ENDPOINT.AS_COMMON_SERVICE.ESNTL_ATCH_FILE}/list/tree`,
{ params },
);
const response = await client.get<EsntlAtchFileMngTreeResponseDto[]>(`${BASE}/list/tree`, {
params,
});
return response.data;
}
async function findPage(
params: EsntlAtchFileMngSearchParams,
): Promise<PageResponse<EsntlAtchFileMng>> {
const response = await client.get<PageResponse<EsntlAtchFileMng>>(
ENDPOINT.AS_COMMON_SERVICE.ESNTL_ATCH_FILE,
{ params },
);
const response = await client.get<PageResponse<EsntlAtchFileMng>>(BASE, { params });
return response.data;
}
async function findById(esntlAtchFileId: string): Promise<EsntlAtchFileMng> {
const response = await client.get<EsntlAtchFileMng>(
`${ENDPOINT.AS_COMMON_SERVICE.ESNTL_ATCH_FILE}/${esntlAtchFileId}`,
);
const response = await client.get<EsntlAtchFileMng>(`${BASE}/${esntlAtchFileId}`);
return response.data;
}
async function create(body: EsntlAtchFileMngSaveRequest): Promise<string> {
const response = await client.post<string>(ENDPOINT.AS_COMMON_SERVICE.ESNTL_ATCH_FILE, body);
const response = await client.post<string>(BASE, body);
return response.data;
}
async function update(esntlAtchFileId: string, body: EsntlAtchFileMngSaveRequest): Promise<string> {
const response = await client.put<string>(
`${ENDPOINT.AS_COMMON_SERVICE.ESNTL_ATCH_FILE}/${esntlAtchFileId}`,
body,
);
const response = await client.put<string>(`${BASE}/${esntlAtchFileId}`, body);
return response.data;
}
async function remove(esntlAtchFileId: string): Promise<string> {
const response = await client.delete<string>(
`${ENDPOINT.AS_COMMON_SERVICE.ESNTL_ATCH_FILE}/${esntlAtchFileId}`,
);
const response = await client.delete<string>(`${BASE}/${esntlAtchFileId}`);
return response.data;
}
+1 -1
View File
@@ -10,7 +10,7 @@ import type {
import { esntlAtchFileMngService } from "./esntl-service";
const esntlAtchFileMngBaseKey = [ENDPOINT.AS_COMMON_SERVICE.ESNTL_ATCH_FILE] as const;
const esntlAtchFileMngBaseKey = [ENDPOINT.AS_SERVICE.ESNTL_ATCH_FILE] as const;
export function useEsntlAtchFileMngTree(params: EsntlAtchFileMngTreeSearchParams) {
return useQuery({
@@ -0,0 +1,86 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type {
DocPrgrsUpdateRequestDto,
FnrsCnsltnApplySaveRequestDto,
FnrsCnsltnMngDetResponseDto,
FnrsCnsltnMngResponseDto,
FnrsCnsltnMngSaveRequestDto,
FnrsCnsltnMngSearchDto,
FnrsCnsltnMngService,
FnrsCnsltnMngUpdateRequestDto,
} from "../model/fnrs-cnsltn-interface";
const BASE = ENDPOINT.AS_SERVICE.FNRS_CNSLTN_MNG;
async function findPage(
searchDto: FnrsCnsltnMngSearchDto,
): Promise<PageResponse<FnrsCnsltnMngResponseDto>> {
const response = await client.get<PageResponse<FnrsCnsltnMngResponseDto>>(BASE, {
params: searchDto,
});
return response.data;
}
async function findById(fnrsCnsltnDocNo: string): Promise<FnrsCnsltnMngDetResponseDto> {
const response = await client.get<FnrsCnsltnMngDetResponseDto>(`${BASE}/${fnrsCnsltnDocNo}`);
return response.data;
}
async function save(body: FnrsCnsltnMngSaveRequestDto): Promise<string> {
const response = await client.post<string>(BASE, body);
return response.data;
}
async function saveApply(body: FnrsCnsltnApplySaveRequestDto): Promise<string> {
const response = await client.post<string>(`${BASE}/save-apply`, body);
return response.data;
}
async function update(
fnrsCnsltnDocNo: string,
body: FnrsCnsltnMngUpdateRequestDto,
): Promise<string> {
const response = await client.put<string>(`${BASE}/${fnrsCnsltnDocNo}`, body);
return response.data;
}
async function techReview(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise<void> {
await client.post<void>(`${BASE}/${id}/tech-review`, body);
}
async function submitSupplement(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise<string> {
const response = await client.post<string>(`${BASE}/${id}/submitsupplement`, body);
return response.data;
}
async function submitReqSupplement(
id: string,
body: FnrsCnsltnApplySaveRequestDto,
): Promise<string> {
const response = await client.post<string>(`${BASE}/${id}/reqsupplement`, body);
return response.data;
}
async function remove(fnrsCnsltnDocNo: string): Promise<void> {
await client.delete<void>(`${BASE}/${fnrsCnsltnDocNo}`);
}
async function changePrgrsSttsComCd(body: DocPrgrsUpdateRequestDto): Promise<void> {
await client.post<void>(`${BASE}/doc/updatePrgrsStts`, body);
}
export const fnrsCnsltnMngService = {
findPage,
findById,
save,
saveApply,
update,
techReview,
submitSupplement,
submitReqSupplement,
remove,
changePrgrsSttsComCd,
} satisfies FnrsCnsltnMngService;
@@ -0,0 +1,131 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import type {
DocPrgrsUpdateRequestDto,
FnrsCnsltnApplySaveRequestDto,
FnrsCnsltnMngSaveRequestDto,
FnrsCnsltnMngSearchDto,
FnrsCnsltnMngUpdateRequestDto,
} from "../model/fnrs-cnsltn-interface";
import { fnrsCnsltnMngService } from "./fnrs-cnsltn-service";
export const fnrsCnsltnMngBaseKey = [ENDPOINT.AS_SERVICE.FNRS_CNSLTN_MNG] as const;
export function useFnrsCnsltnMngPage(params: FnrsCnsltnMngSearchDto) {
return useQuery({
queryKey: [...fnrsCnsltnMngBaseKey, "findPage", params],
queryFn: () => fnrsCnsltnMngService.findPage(params),
});
}
export function useFnrsCnsltnMng(fnrsCnsltnDocNo: string | null | undefined) {
return useQuery({
queryKey: [...fnrsCnsltnMngBaseKey, "findById", fnrsCnsltnDocNo],
queryFn: () => fnrsCnsltnMngService.findById(fnrsCnsltnDocNo!),
enabled: Boolean(fnrsCnsltnDocNo),
});
}
export function useSaveFnrsCnsltnMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (body: FnrsCnsltnMngSaveRequestDto) => fnrsCnsltnMngService.save(body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey });
},
});
}
export function useSaveApplyFnrsCnsltnMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (body: FnrsCnsltnApplySaveRequestDto) => fnrsCnsltnMngService.saveApply(body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey });
},
});
}
export function useUpdateFnrsCnsltnMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
fnrsCnsltnDocNo,
body,
}: {
fnrsCnsltnDocNo: string;
body: FnrsCnsltnMngUpdateRequestDto;
}) => fnrsCnsltnMngService.update(fnrsCnsltnDocNo, body),
onSuccess: (_data, { fnrsCnsltnDocNo }) => {
queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey });
queryClient.invalidateQueries({
queryKey: [...fnrsCnsltnMngBaseKey, "findById", fnrsCnsltnDocNo],
});
},
});
}
export function useTechReviewFnrsCnsltnMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, body }: { id: string; body: FnrsCnsltnApplySaveRequestDto }) =>
fnrsCnsltnMngService.techReview(id, body),
onSuccess: (_data, { id }) => {
queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey });
queryClient.invalidateQueries({
queryKey: [...fnrsCnsltnMngBaseKey, "findById", id],
});
},
});
}
export function useSubmitSupplementFnrsCnsltnMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, body }: { id: string; body: FnrsCnsltnApplySaveRequestDto }) =>
fnrsCnsltnMngService.submitSupplement(id, body),
onSuccess: (_data, { id }) => {
queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey });
queryClient.invalidateQueries({
queryKey: [...fnrsCnsltnMngBaseKey, "findById", id],
});
},
});
}
export function useSubmitReqSupplementFnrsCnsltnMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, body }: { id: string; body: FnrsCnsltnApplySaveRequestDto }) =>
fnrsCnsltnMngService.submitReqSupplement(id, body),
onSuccess: (_data, { id }) => {
queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey });
queryClient.invalidateQueries({
queryKey: [...fnrsCnsltnMngBaseKey, "findById", id],
});
},
});
}
export function useDeleteFnrsCnsltnMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (fnrsCnsltnDocNo: string) => fnrsCnsltnMngService.remove(fnrsCnsltnDocNo),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey });
},
});
}
export function useChangePrgrsSttsComCd() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (body: DocPrgrsUpdateRequestDto) => fnrsCnsltnMngService.changePrgrsSttsComCd(body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey });
},
});
}
+39
View File
@@ -0,0 +1,39 @@
export { fnrsCnsltnMngService } from "./api/fnrs-cnsltn-service";
export { fnrsCnsltnMngBaseKey } from "./api/queries";
export {
useChangePrgrsSttsComCd,
useDeleteFnrsCnsltnMng,
useFnrsCnsltnMng,
useFnrsCnsltnMngPage,
useSaveApplyFnrsCnsltnMng,
useSaveFnrsCnsltnMng,
useSubmitReqSupplementFnrsCnsltnMng,
useSubmitSupplementFnrsCnsltnMng,
useTechReviewFnrsCnsltnMng,
useUpdateFnrsCnsltnMng,
} from "./api/queries";
export type {
AtflMngClientResponseDto,
BizCstItem,
BizCstTreeDto,
BizSclTreeDto,
BzScaleItem,
CklstResultItem,
CklstRvwRsltTreeDto,
DocPrgrsUpdateRequestDto,
ElaprDocPrgrsClientResponseDto,
EsntlAtchFileItem,
EsntlAtchFileMngTreeResponseDto,
FnrsCnsltnApplySaveRequestDto,
FnrsCnsltnMngDetResponseDto,
FnrsCnsltnMngResponseDto,
FnrsCnsltnMngSaveRequestDto,
FnrsCnsltnMngSearchDto,
FnrsCnsltnMngService,
FnrsCnsltnMngUpdateRequestDto,
FnrsCnsltnRvwOpnnResponseDto,
FnrsCnsltnRvwOpnnSaveRequestDto,
FnrsCnsltnSplmntDmndItem,
FnrsCnsltnSplmntDmndResponseDto,
FnrsSharItem,
} from "./model/fnrs-cnsltn-interface";
@@ -0,0 +1,177 @@
import type {
AtflMngClientResponseDto,
BizCstSaveBase,
BzScaleSaveBase,
CklstResultSaveBase,
DocPrgrsUpdateRequestDto,
EsntlAtchFileSaveBase,
PageableParams,
PageResponse,
PbadmsApplySaveHeader,
PbadmsMngDetResponseBase,
RvwOpnnResponseExtension,
RvwOpnnSaveBase,
SplmntDmndResponseExtension,
SplmntDmndSaveBase,
} from "@/shared/model";
import type { FnrsSharBizCstTreeDto } from "@/entities/fnrs-shar-se";
export type {
AtflMngClientResponseDto,
BizCstTreeDto,
BizSclTreeDto,
CklstRvwRsltTreeDto,
DocPrgrsUpdateRequestDto,
ElaprDocPrgrsClientResponseDto,
EsntlAtchFileMngTreeResponseDto,
} from "@/shared/model";
/** 백엔드 FnrsCnsltnMngSaveRequestDto */
export interface FnrsCnsltnMngSaveRequestDto {
fnrsCnsltnDocNo?: string;
admdstCd?: string;
envoSeComCd?: string;
taskSeComCd?: string;
cfmtnBgtBizNo?: string;
bgtAplyYr?: string;
bizNm?: string;
bizBgngYr?: string;
bizEndYr?: string;
bizCn?: string;
aplcntNm?: string;
aplcntMblTelno?: string;
aplcntOgdpNm?: string;
aplcntEmlAddr?: string;
dsgnrNm?: string;
dsgnrTelno?: string;
dsgnrOgdpNm?: string;
dsgnrEmlAddr?: string;
atchFileGroupId?: string;
sharAtchFileGroupId?: string;
prgrsSttsComCd?: string;
rtlztnFcltSeComCd?: string;
rtlztnUsgSeComCd?: string;
}
/** 백엔드 FnrsCnsltnMngUpdateRequestDto */
export type FnrsCnsltnMngUpdateRequestDto = Omit<FnrsCnsltnMngSaveRequestDto, "fnrsCnsltnDocNo">;
/** 백엔드 FnrsCnsltnMngSearchDto + Pageable */
export interface FnrsCnsltnMngSearchDto extends PageableParams {
fnrsCnsltnDocNo?: string;
admdstCd?: string;
envoSeComCd?: string;
taskSeComCd?: string;
cfmtnBgtBizNo?: string;
bgtAplyYr?: string;
bizNm?: string;
bizBgngYr?: string;
bizEndYr?: string;
bizCn?: string;
aplcntNm?: string;
aplcntMblTelno?: string;
aplcntOgdpNm?: string;
aplcntEmlAddr?: string;
dsgnrNm?: string;
dsgnrTelno?: string;
dsgnrOgdpNm?: string;
dsgnrEmlAddr?: string;
atchFileGroupId?: string;
sharAtchFileGroupId?: string;
prgrsSttsComCd?: string;
}
interface FnrsCnsltnMngResponseExtension {
fnrsCnsltnDocNo: string;
envoSeCdNm?: string;
taskSeCdNm?: string;
prgrsSttsCdNm?: string;
regDt?: string;
rgtr?: string;
rgtrNm?: string;
mdfcnDt?: string;
sido?: string;
gugun?: string;
}
/** 백엔드 FnrsCnsltnMngResponseDto */
export type FnrsCnsltnMngResponseDto = Omit<FnrsCnsltnMngSaveRequestDto, "fnrsCnsltnDocNo"> &
FnrsCnsltnMngResponseExtension;
export type BzScaleItem = BzScaleSaveBase;
export type BizCstItem = BizCstSaveBase;
export interface CklstResultItem extends CklstResultSaveBase {
selected?: string;
}
export type EsntlAtchFileItem = EsntlAtchFileSaveBase;
export type FnrsCnsltnSplmntDmndItem = SplmntDmndSaveBase;
/** 백엔드 FnrsCnsltnApplySaveRequestDto.FnrsSharItem */
export interface FnrsSharItem {
fnrsSharSeCd?: string;
exstnAmt?: number;
chgRqtAmt?: number;
ajmtAmt?: number;
rmrkCn?: string;
envoRmrkCn?: string;
}
/** 백엔드 FnrsCnsltnRvwOpnnSaveRequestDto */
export interface FnrsCnsltnRvwOpnnSaveRequestDto extends RvwOpnnSaveBase {
fnrsCnsltnDocNo: string;
rvwOpnnSn: number;
}
/** 백엔드 FnrsCnsltnRvwOpnnResponseDto */
export type FnrsCnsltnRvwOpnnResponseDto = Partial<
Pick<FnrsCnsltnRvwOpnnSaveRequestDto, "fnrsCnsltnDocNo" | "rvwOpnnSn">
> &
Omit<FnrsCnsltnRvwOpnnSaveRequestDto, "fnrsCnsltnDocNo" | "rvwOpnnSn"> &
RvwOpnnResponseExtension;
/** 백엔드 FnrsCnsltnSplmntDmndResponseDto */
export type FnrsCnsltnSplmntDmndResponseDto = FnrsCnsltnSplmntDmndItem &
SplmntDmndResponseExtension & {
fnrsCnsltnDocNo?: string;
};
/** 백엔드 FnrsCnsltnMngDetResponseDto */
export type FnrsCnsltnMngDetResponseDto = PbadmsMngDetResponseBase<
FnrsCnsltnMngResponseDto,
FnrsCnsltnRvwOpnnResponseDto,
FnrsCnsltnSplmntDmndResponseDto
> & {
fnrsSharList?: FnrsSharBizCstTreeDto[];
sharAtchFileList?: AtflMngClientResponseDto[];
};
/** 백엔드 FnrsCnsltnApplySaveRequestDto */
export interface FnrsCnsltnApplySaveRequestDto extends PbadmsApplySaveHeader {
fnrsCnsltnMng: FnrsCnsltnMngSaveRequestDto;
bzScaleList?: BzScaleItem[];
bizCstList?: BizCstItem[];
cklstResultList?: CklstResultItem[];
cklstResultMap?: Record<string, CklstResultItem[]>;
esntlAtchFileList?: EsntlAtchFileItem[];
rvwOpnnList?: FnrsCnsltnRvwOpnnSaveRequestDto[];
splmntDmndList?: FnrsCnsltnSplmntDmndItem[];
fnrsSharList?: FnrsSharItem[];
}
/** Controller public 메서드명·시그니처와 동일 */
export interface FnrsCnsltnMngService {
findPage(searchDto: FnrsCnsltnMngSearchDto): Promise<PageResponse<FnrsCnsltnMngResponseDto>>;
findById(fnrsCnsltnDocNo: string): Promise<FnrsCnsltnMngDetResponseDto>;
save(body: FnrsCnsltnMngSaveRequestDto): Promise<string>;
saveApply(body: FnrsCnsltnApplySaveRequestDto): Promise<string>;
update(fnrsCnsltnDocNo: string, body: FnrsCnsltnMngUpdateRequestDto): Promise<string>;
techReview(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise<void>;
submitSupplement(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise<string>;
submitReqSupplement(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise<string>;
remove(fnrsCnsltnDocNo: string): Promise<void>;
changePrgrsSttsComCd(body: DocPrgrsUpdateRequestDto): Promise<void>;
}
@@ -0,0 +1,70 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import {
FNRS_SHAR_SE_SAVE_MODE,
type FnrsSharBizCstTreeDto,
type FnrsSharSeMngResponseDto,
type FnrsSharSeMngSaveRequestDto,
type FnrsSharSeMngSearchDto,
type FnrsSharSeMngService,
type FnrsSharSeMngTreeDto,
} from "../model/fnrs-shar-se-interface";
const BASE = ENDPOINT.AS_SERVICE.FNRS_SHAR_SE;
async function findAll(
params: FnrsSharSeMngSearchDto,
): Promise<PageResponse<FnrsSharSeMngResponseDto>> {
const response = await client.get<PageResponse<FnrsSharSeMngResponseDto>>(BASE, { params });
return response.data;
}
async function findTree(): Promise<FnrsSharSeMngTreeDto[]> {
const response = await client.get<FnrsSharSeMngTreeDto[]>(`${BASE}/list/tree`);
return response.data;
}
async function findById(fnrsSharSeCd: string): Promise<FnrsSharSeMngResponseDto> {
const response = await client.get<FnrsSharSeMngResponseDto>(`${BASE}/${fnrsSharSeCd}`);
return response.data;
}
async function saveMulti(
body: FnrsSharSeMngSaveRequestDto[],
): Promise<FnrsSharSeMngResponseDto[]> {
const response = await client.post<FnrsSharSeMngResponseDto[]>(BASE, body);
return response.data;
}
async function update(fnrsSharSeCd: string, body: FnrsSharSeMngSaveRequestDto): Promise<string> {
const response = await client.put<string>(`${BASE}/${fnrsSharSeCd}`, {
...body,
fnrsSharSeCd,
mode: FNRS_SHAR_SE_SAVE_MODE.UPDATE,
});
return response.data;
}
async function deleteFnrsSharSeCd(fnrsSharSeCd: string): Promise<string> {
const response = await client.delete<string>(`${BASE}/${fnrsSharSeCd}`);
return response.data;
}
async function findTreeFnrsShar(fnrsCnsltnDocNo: string): Promise<FnrsSharBizCstTreeDto[]> {
const response = await client.get<FnrsSharBizCstTreeDto[]>(
`${BASE}/list/tree/fnrs-shar/${fnrsCnsltnDocNo}`,
);
return response.data;
}
export const fnrsSharSeMngService = {
findAll,
findTree,
findById,
saveMulti,
update,
delete: deleteFnrsSharSeCd,
findTreeFnrsShar,
} satisfies FnrsSharSeMngService;
@@ -0,0 +1,85 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import type {
FnrsSharSeMngSaveRequestDto,
FnrsSharSeMngSearchDto,
} from "../model/fnrs-shar-se-interface";
import { fnrsSharSeMngService } from "./fnrs-shar-se-service";
const fnrsSharSeMngBaseKey = [ENDPOINT.AS_SERVICE.FNRS_SHAR_SE] as const;
export function useFnrsSharSeMngPage(params: FnrsSharSeMngSearchDto) {
return useQuery({
queryKey: [...fnrsSharSeMngBaseKey, "findAll", params],
queryFn: () => fnrsSharSeMngService.findAll(params),
});
}
export function useFnrsSharSeMngTree() {
return useQuery({
queryKey: [...fnrsSharSeMngBaseKey, "findTree"],
queryFn: () => fnrsSharSeMngService.findTree(),
staleTime: Infinity,
});
}
export function useFnrsSharSeMng(fnrsSharSeCd: string | null | undefined) {
return useQuery({
queryKey: [...fnrsSharSeMngBaseKey, "findById", fnrsSharSeCd],
queryFn: () => fnrsSharSeMngService.findById(fnrsSharSeCd!),
enabled: Boolean(fnrsSharSeCd),
});
}
export function useSaveMultiFnrsSharSeMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (body: FnrsSharSeMngSaveRequestDto[]) => fnrsSharSeMngService.saveMulti(body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: fnrsSharSeMngBaseKey });
},
});
}
export function useUpdateFnrsSharSeMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
fnrsSharSeCd,
body,
}: {
fnrsSharSeCd: string;
body: FnrsSharSeMngSaveRequestDto;
}) => fnrsSharSeMngService.update(fnrsSharSeCd, body),
onSuccess: (_data, { fnrsSharSeCd }) => {
queryClient.invalidateQueries({ queryKey: fnrsSharSeMngBaseKey });
queryClient.invalidateQueries({
queryKey: [...fnrsSharSeMngBaseKey, "findById", fnrsSharSeCd],
});
},
});
}
export function useDeleteFnrsSharSeMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (fnrsSharSeCd: string) => fnrsSharSeMngService.delete(fnrsSharSeCd),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: fnrsSharSeMngBaseKey });
},
});
}
export function useFnrsSharBizCstTree(fnrsCnsltnDocNo: string | null | undefined) {
return useQuery({
queryKey: [...fnrsSharSeMngBaseKey, "findTreeFnrsShar", fnrsCnsltnDocNo],
queryFn: () => fnrsSharSeMngService.findTreeFnrsShar(fnrsCnsltnDocNo!),
enabled: Boolean(fnrsCnsltnDocNo),
});
}
@@ -0,0 +1,20 @@
export { fnrsSharSeMngService } from "./api/fnrs-shar-se-service";
export {
useDeleteFnrsSharSeMng,
useFnrsSharBizCstTree,
useFnrsSharSeMng,
useFnrsSharSeMngPage,
useFnrsSharSeMngTree,
useSaveMultiFnrsSharSeMng,
useUpdateFnrsSharSeMng,
} from "./api/queries";
export type {
FnrsSharBizCstTreeDto,
FnrsSharSeMngResponseDto,
FnrsSharSeMngSaveRequestDto,
FnrsSharSeMngSearchDto,
FnrsSharSeMngService,
FnrsSharSeMngTreeDto,
FnrsSharSeSaveMode,
} from "./model/fnrs-shar-se-interface";
export { FNRS_SHAR_SE_SAVE_MODE } from "./model/fnrs-shar-se-interface";
@@ -0,0 +1,83 @@
import type { PageableParams, PageResponse } from "@/shared/model";
export const FNRS_SHAR_SE_SAVE_MODE = {
CREATE: "C",
UPDATE: "U",
DELETE: "D",
} as const;
export type FnrsSharSeSaveMode = (typeof FNRS_SHAR_SE_SAVE_MODE)[keyof typeof FNRS_SHAR_SE_SAVE_MODE];
/** 백엔드 FnrsSharSeMngResponseDto */
export interface FnrsSharSeMngResponseDto {
fnrsSharSeCd: string;
fnrsSharSeCdNm: string;
upFnrsSharSeCd?: string;
cdLvNo: number;
useYn: string;
expln?: string;
sortSeq?: number;
rgtr?: string;
rgtrNm?: string;
mdfr?: string;
regDt?: string;
mdfcnDt?: string;
}
/** 백엔드 FnrsSharSeMngSearchDto + Pageable */
export interface FnrsSharSeMngSearchDto extends PageableParams {
fnrsSharSeCd?: string;
fnrsSharSeCdNm?: string;
useYn?: string;
}
/** 백엔드 FnrsSharSeMngSaveRequestDto */
export interface FnrsSharSeMngSaveRequestDto {
fnrsSharSeCd: string;
mode: FnrsSharSeSaveMode;
fnrsSharSeCdNm: string;
upFnrsSharSeCd?: string;
cdLvNo: number;
useYn?: string;
expln?: string;
sortSeq?: number;
}
/** 백엔드 FnrsSharSeMngTreeDto */
export interface FnrsSharSeMngTreeDto {
indFnrsSharSeCdNm: string;
fnrsSharSeCd: string;
fnrsSharSeCdNm: string;
upFnrsSharSeCd?: string;
useYn: string;
treeLevel: number;
treePath: string;
sortPath: string;
}
/** 백엔드 FnrsSharBizCstTreeDto */
export interface FnrsSharBizCstTreeDto {
indFnrsSharSeCdNm: string;
fnrsSharSeCd: string;
fnrsSharSeCdNm: string;
useYn: string;
treeLevel: number;
treePath: string;
sortPath: string;
exstnAmt?: number;
chgRqtAmt?: number;
ajmtAmt?: number;
rmrkCn?: string;
envoRmrkCn?: string;
}
/** Controller public 메서드명·시그니처와 동일 */
export interface FnrsSharSeMngService {
findAll(params: FnrsSharSeMngSearchDto): Promise<PageResponse<FnrsSharSeMngResponseDto>>;
findTree(): Promise<FnrsSharSeMngTreeDto[]>;
findById(fnrsSharSeCd: string): Promise<FnrsSharSeMngResponseDto>;
saveMulti(body: FnrsSharSeMngSaveRequestDto[]): Promise<FnrsSharSeMngResponseDto[]>;
update(fnrsSharSeCd: string, body: FnrsSharSeMngSaveRequestDto): Promise<string>;
delete(fnrsSharSeCd: string): Promise<string>;
findTreeFnrsShar(fnrsCnsltnDocNo: string): Promise<FnrsSharBizCstTreeDto[]>;
}
@@ -1,41 +1,109 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type {
AdstZoneResponseDto,
DocPrgrsUpdateRequestDto,
InstlSncnApplySaveRequestDto,
InstlSncnMng,
InstlSncnMngDetResponse,
InstlSncnMngResponseDto,
InstlSncnMngSaveRequestDto,
InstlSncnMngSearchParams,
InstlSncnMngService,
SpringPage,
InstlSncnMngDetResponse,
InstlSncnMngUpdateRequestDto,
} from "../model/instl-sncn-interface";
export class InstlSncnApiService implements InstlSncnMngService {
public async getPage(params: InstlSncnMngSearchParams): Promise<SpringPage<InstlSncnMng>> {
const response = await client.get<SpringPage<InstlSncnMng>>(ENDPOINT.AS_INSTL_SERVICE.INSTL_SNCN, {
params,
});
return response.data;
}
async function getSidoCombo(): Promise<AdstZoneResponseDto[]> {
const response = await client.get<AdstZoneResponseDto[]>(
`${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/sido`,
);
public async getById(id: string): Promise<InstlSncnMngDetResponse> {
const response = await client.get<InstlSncnMngDetResponse>(`${ENDPOINT.AS_INSTL_SERVICE.INSTL_SNCN}/${id}`);
return response.data;
}
public async saveApply(payload: InstlSncnApplySaveRequestDto): Promise<string> {
const response = await client.post<string>(
`${ENDPOINT.AS_INSTL_SERVICE.INSTL_SNCN}/save-apply`,
payload,
);
return response.data;
}
public async techReview(payload: InstlSncnApplySaveRequestDto): Promise<string> {
const response = await client.post<string>(
`${ENDPOINT.AS_INSTL_SERVICE.INSTL_SNCN}/tech-review`,
payload,
);
return response.data;
}
return response.data;
}
export const InstlSncnApi = new InstlSncnApiService();
async function getSigunguCombo(sidoCd: string): Promise<AdstZoneResponseDto[]> {
const response = await client.get<AdstZoneResponseDto[]>(
`${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/sigungu`,
{ params: { sidoCd } },
);
return response.data;
}
const BASE = ENDPOINT.AS_SERVICE.INSTL_SNCN;
async function getPage(
params: InstlSncnMngSearchParams,
): Promise<PageResponse<InstlSncnMngResponseDto>> {
const response = await client.get<PageResponse<InstlSncnMngResponseDto>>(BASE, {
params,
});
return response.data;
}
async function getById(id: string): Promise<InstlSncnMngDetResponse> {
const response = await client.get<InstlSncnMngDetResponse>(`${BASE}/${id}`);
return response.data;
}
async function save(payload: InstlSncnMngSaveRequestDto): Promise<string> {
const response = await client.post<string>(BASE, payload);
return response.data;
}
async function saveApply(payload: InstlSncnApplySaveRequestDto): Promise<string> {
const response = await client.post<string>(`${BASE}/save-apply`, payload);
return response.data;
}
async function update(id: string, payload: InstlSncnMngUpdateRequestDto): Promise<void> {
await client.put(`${BASE}/${id}`, payload);
}
async function techReview(id: string, payload: InstlSncnApplySaveRequestDto): Promise<void> {
await client.post(`${BASE}/${id}/tech-review`, payload);
}
async function submitSupplement(
id: string,
payload: InstlSncnApplySaveRequestDto,
): Promise<string> {
const response = await client.post<string>(`${BASE}/${id}/submitsupplement`, payload);
return response.data;
}
async function submitReqSupplement(
id: string,
payload: InstlSncnApplySaveRequestDto,
): Promise<string> {
const response = await client.post<string>(`${BASE}/${id}/reqsupplement`, payload);
return response.data;
}
async function remove(id: string): Promise<void> {
await client.delete(`${BASE}/${id}`);
}
async function changePrgrsSttsComCd(payload: DocPrgrsUpdateRequestDto): Promise<void> {
await client.post(`${BASE}/doc/updatePrgrsStts`, payload);
}
export const instlSncnService = {
getPage,
getById,
save,
saveApply,
update,
techReview,
submitSupplement,
submitReqSupplement,
remove,
changePrgrsSttsComCd,
} satisfies InstlSncnMngService;
// TODO: st-anls-mst service 로 분리
export const instlSncnRegionService = {
getSidoCombo,
getSigunguCombo,
};
@@ -0,0 +1,152 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import type {
DocPrgrsUpdateRequestDto,
InstlSncnApplySaveRequestDto,
InstlSncnMngSaveRequestDto,
InstlSncnMngSearchParams,
InstlSncnMngUpdateRequestDto,
} from "../model/instl-sncn-interface";
import { instlSncnRegionService, instlSncnService } from "./instl-sncn-service";
export const instlSncnKeys = {
all: ["instl-sncn"] as const,
list: () => [...instlSncnKeys.all, "list"] as const,
detail: (id: string) => [...instlSncnKeys.all, "detail", id] as const,
sidoCombo: () => [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, "sido"] as const,
sigunguCombo: (sidoCd: string) =>
[ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, "sigungu", sidoCd] as const,
};
export function useInstlSncnList(params: InstlSncnMngSearchParams) {
return useQuery({
queryKey: [...instlSncnKeys.list(), params],
queryFn: () => instlSncnService.getPage(params),
});
}
export function useInstlSncnDetail(id: string) {
return useQuery({
queryKey: instlSncnKeys.detail(id),
queryFn: () => instlSncnService.getById(id),
enabled: Boolean(id),
});
}
export function useInstlSncnSidoCombo() {
return useQuery({
queryKey: instlSncnKeys.sidoCombo(),
queryFn: () => instlSncnRegionService.getSidoCombo(),
});
}
export function useInstlSncnSigunguCombo(sidoCd: string) {
return useQuery({
queryKey: instlSncnKeys.sigunguCombo(sidoCd),
queryFn: () => instlSncnRegionService.getSigunguCombo(sidoCd),
enabled: Boolean(sidoCd),
});
}
export function useInstlSncnSave() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (payload: InstlSncnMngSaveRequestDto) => instlSncnService.save(payload),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
},
});
}
export function useInstlSncnSaveApply() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (payload: InstlSncnApplySaveRequestDto) => instlSncnService.saveApply(payload),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
},
});
}
export function useInstlSncnUpdate() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, payload }: { id: string; payload: InstlSncnMngUpdateRequestDto }) =>
instlSncnService.update(id, payload),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
queryClient.invalidateQueries({ queryKey: instlSncnKeys.detail(variables.id) });
},
});
}
export function useInstlSncnTechReview() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, payload }: { id: string; payload: InstlSncnApplySaveRequestDto }) =>
instlSncnService.techReview(id, payload),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
queryClient.invalidateQueries({ queryKey: instlSncnKeys.detail(variables.id) });
},
});
}
export function useInstlSncnSubmitSupplement() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, payload }: { id: string; payload: InstlSncnApplySaveRequestDto }) =>
instlSncnService.submitSupplement(id, payload),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
queryClient.invalidateQueries({ queryKey: instlSncnKeys.detail(variables.id) });
},
});
}
export function useInstlSncnSubmitReqSupplement() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, payload }: { id: string; payload: InstlSncnApplySaveRequestDto }) =>
instlSncnService.submitReqSupplement(id, payload),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
queryClient.invalidateQueries({ queryKey: instlSncnKeys.detail(variables.id) });
},
});
}
export function useInstlSncnRemove() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => instlSncnService.remove(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
},
});
}
export function useInstlSncnChangePrgrsStts() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (payload: DocPrgrsUpdateRequestDto) =>
instlSncnService.changePrgrsSttsComCd(payload),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
queryClient.invalidateQueries({
queryKey: instlSncnKeys.detail(variables.docNo),
});
},
});
}
+28 -4
View File
@@ -1,12 +1,36 @@
export { InstlSncnApi } from "./api/instl-sncn-service";
export { instlSncnRegionService, instlSncnService } from "./api/instl-sncn-service";
export {
instlSncnKeys,
useInstlSncnDetail,
useInstlSncnList,
useInstlSncnSidoCombo,
useInstlSncnSigunguCombo,
useInstlSncnSubmitReqSupplement,
useInstlSncnSubmitSupplement,
useInstlSncnTechReview,
} from "./api/queries";
export {
type AdstZoneResponseDto,
type AtflMngClientResponseDto,
type BizCstTreeDto,
type BizSclTreeDto,
type CklstRvwRsltTreeDto,
type DocPrgrsUpdateRequestDto,
type ElaprDocPrgrsClientResponseDto,
type EsntlAtchFileMngTreeResponseDto,
INSTL_SNCN_REGISTER_SELECTION_KEY,
type InstlSncnApplySaveRequestDto,
type InstlSncnBizCstSaveRequestDto,
type InstlSncnBzScaleSaveDto,
type InstlSncnCklstResultSaveDto,
type InstlSncnMng,
type InstlSncnEsntlAtchFileSaveDto,
type InstlSncnMngDetResponse,
type InstlSncnMngResponseDto,
type InstlSncnMngSaveRequestDto,
type InstlSncnMngSearchParams,
type SpringPage,
type ElaprDocPrgrsClientResponseDto,
type InstlSncnRvwOpnnResponseDto,
type InstlSncnRvwOpnnSaveRequestDto,
type InstlSncnSplmntDmndResponseDto,
type InstlSncnSplmntDmndSaveDto,
type ReviewStepCode,
} from "./model/instl-sncn-interface";
@@ -1,78 +1,150 @@
export const INSTL_SNCN_REGISTER_SELECTION_KEY = "instlSncnRegisterSelection";
import type {
AtflMngClientResponseDto,
BizCstSaveBase,
BizCstTreeDto,
BizSclTreeDto,
BzScaleSaveBase,
CklstResultSaveBase,
DocPrgrsUpdateRequestDto,
ElaprDocPrgrsClientResponseDto,
EsntlAtchFileMngTreeResponseDto,
EsntlAtchFileSaveBase,
PageableParams,
PageResponse,
PbadmsApplySaveHeader,
PbadmsMngDetResponseBase,
RvwOpnnResponseExtension,
RvwOpnnSaveBase,
SplmntDmndResponseExtension,
SplmntDmndSaveBase,
} from "@/shared/model";
export interface SpringPage<T> {
content: T[];
totalElements: number;
totalPages: number;
number: number;
size: number;
first: boolean;
last: boolean;
numberOfElements: number;
export type {
AtflMngClientResponseDto,
BizCstTreeDto,
BizSclTreeDto,
CklstRvwRsltTreeDto,
DocPrgrsUpdateRequestDto,
ElaprDocPrgrsClientResponseDto,
EsntlAtchFileMngTreeResponseDto,
} from "@/shared/model";
/** 행정구역(시도·시군구) 콤보 응답 DTO */
export interface AdstZoneResponseDto {
/** 행정구역 코드 */
adstCd: string;
/** 행정구역 명 */
adstNm: string;
}
export interface InstlSncnMngSearchParams {
export const INSTL_SNCN_REGISTER_SELECTION_KEY = "instlSncnRegisterSelection";
export interface InstlSncnMngSearchParams extends PageableParams {
bizNm?: string;
taskSeComCd?: string;
sidoComCd?: string;
sggComCd?: string;
page?: number;
size?: number;
sort?: string;
}
export interface InstlSncnMng {
gugun: string;
sido: string;
instlSncnDocNo: string;
admdstCd: string;
sidoNm?: string;
sggNm?: string;
chrgrNm?: string;
instNm?: string;
envoSeComCd: string;
/** 설치인가관리 저장 요청 DTO */
export interface InstlSncnMngSaveRequestDto {
/** 설치인가문서번호 */
instlSncnDocNo?: string;
/** 행정구역코드 */
admdstCd?: string;
/** 환경청구분공통코드 */
envoSeComCd?: string;
/** 인가구분공통코드 */
sncnSeComCd: string;
envoSeCdNm: string;
/** 업무구분공통코드 */
taskSeComCd: string;
taskSeCdNm: string;
bgtBizCd: string;
bgtAplyYr: string;
/** 예산사업코드 */
bgtBizCd?: string;
/** 예산신청연도 */
bgtAplyYr?: string;
/** 사업명 */
bizNm: string;
/** 사업시작연도 */
bizBgngYr: string;
/** 사업종료연도 */
bizEndYr: string;
/** 사업내용 */
bizCn: string;
aplcntNm: string;
aplcntMblTelno: string;
aplcntOgdpNm: string;
aplcntEmlAddr: string;
/** 신청자명 */
aplcntNm?: string;
/** 신청자휴대전화번호 */
aplcntMblTelno?: string;
/** 신청자소속명 */
aplcntOgdpNm?: string;
/** 신청자이메일주소 */
aplcntEmlAddr?: string;
/** 설계자명 */
dsgnrNm: string;
/** 설계자전화번호 */
dsgnrTelno: string;
/** 설계자소속명 */
dsgnrOgdpNm: string;
/** 설계자이메일주소 */
dsgnrEmlAddr: string;
atchFileGroupId: string;
prgrsSttsComCd: string;
/** 첨부파일그룹아이디 */
atchFileGroupId?: string;
/** 진행상태공통코드 */
prgrsSttsComCd?: string;
/** 재이용시설구분공통코드 */
rtlztnFcltSeComCd?: string;
/** 재이용용도구분공통코드 */
rtlztnUsgSeComCd?: string;
}
/** 설치인가관리 수정 요청 DTO */
export type InstlSncnMngUpdateRequestDto = InstlSncnMngSaveRequestDto;
/** 설치인가관리 응답 DTO */
export interface InstlSncnMngResponseDto extends InstlSncnMngSaveRequestDto {
/** 설치인가문서번호 (필수로 재정의) */
instlSncnDocNo: string;
/** 환경청구분공통코드명 */
envoSeCdNm: string;
/** 인가구분공통코드명 */
sncnSeCdNm: string;
/** 업무구분공통코드명 */
taskSeCdNm: string;
/** 진행상태공통코드명 */
prgrsSttsCdNm: string;
/** 재이용시설구분공통코드명 */
rtlztnFcltSeComNm: string;
/** 재이용용도구분공통코드명 */
rtlztnUsgSeComNm: string;
/** 등록일시 */
regDt: string;
mdfcnDt: string;
/** 등록자 UUID */
rgtr: string;
/** 등록자명 */
rgtrNm: string;
/** 수정 일시 */
mdfcnDt: string;
/** 시도 */
sido: string;
/** 구군 */
gugun: string;
}
/** 설치인가 신청 - 사업규모 저장 DTO(배열 원소) */
export interface InstlSncnBzScaleSaveDto {
export interface InstlSncnBzScaleSaveDto extends BzScaleSaveBase {
/** 공종구분코드 */
cnstpSeCd: string;
/** 기본계획(A) */
/** 기본계획수량 */
bscPl: number;
/** 예산반영(B) */
/** 예산반영수량 */
budgetReflct: number;
/** 금회설계시(C) */
/** 금회설계수량 */
thtiDsgn: number;
/** 증감(C-B) */
diff: number;
/** 증감(C-B) — 클라이언트 계산용, API 미전송 */
diff?: number;
}
/** 설치인가 신청 - 총사업비 저장 DTO(배열 원소) */
export interface InstlSncnBizCstSaveRequestDto {
export interface InstlSncnBizCstSaveRequestDto extends BizCstSaveBase {
/** 회계구분코드 */
acntgSeCd: string;
/** 기본계획금액 */
@@ -84,66 +156,101 @@ export interface InstlSncnBizCstSaveRequestDto {
}
/** 설치인가 신청 - 체크리스트 결과 저장 DTO(배열 원소) */
export interface InstlSncnCklstResultSaveDto {
export interface InstlSncnCklstResultSaveDto extends CklstResultSaveBase {
/** 점검표일련번호 */
cklstSn: number;
/** 점검표항목일련번호 */
cklstArtclSn: number;
/** 점검표검토일련번호 */
cklstRvwSn: number;
rvwStpComCd: string;
/** 선택값(Y/N/null) */
selected: "Y" | "N" | null;
/** 담당자검토내용(기술검토의견) */
picRvwCn?: string | null;
}
/** 설치인가 신청 - 필수첨부파일 저장 DTO(배열 원소) */
export interface InstlSncnEsntlAtchFileSaveDto {
export interface InstlSncnEsntlAtchFileSaveDto extends EsntlAtchFileSaveBase {
/** 필수첨부파일아이디 */
esntlAtchFileId: string;
/** 첨부파일그룹아이디 */
atchFileGroupId: string;
}
/** 설치인가검토의견 저장 요청 DTO */
export interface InstlSncnRvwOpnnSaveRequestDto extends RvwOpnnSaveBase {
/** 설치인가문서번호 */
instlSncnDocNo: string;
}
/** 설치인가 보완요구 저장 DTO(배열 원소) */
export type InstlSncnSplmntDmndSaveDto = SplmntDmndSaveBase;
/** 설치인가 보완요구 응답 DTO */
export type InstlSncnSplmntDmndResponseDto = InstlSncnSplmntDmndSaveDto &
SplmntDmndResponseExtension & {
instlSncnDocNo?: string;
};
/** 설치인가 신청 - 통합 저장 요청 DTO */
export interface InstlSncnApplySaveRequestDto {
instlSncnMng: InstlSncnMng;
bzScaleList: InstlSncnBzScaleSaveDto[];
bizCstList: InstlSncnBizCstSaveRequestDto[];
cklstResultList: InstlSncnCklstResultSaveDto[];
esntlAtchFileList: InstlSncnEsntlAtchFileSaveDto[];
export interface InstlSncnApplySaveRequestDto extends PbadmsApplySaveHeader {
/** 설치인가관리 정보 */
instlSncnMng: InstlSncnMngSaveRequestDto;
/** 사업규모 목록 */
bzScaleList?: InstlSncnBzScaleSaveDto[];
/** 총사업비 목록 */
bizCstList?: InstlSncnBizCstSaveRequestDto[];
/** 체크리스트 검토결과 목록 */
cklstResultList?: InstlSncnCklstResultSaveDto[];
/** 체크리스트 검토결과 맵 (rvwStpComCd -> 목록) */
cklstResultMap?: Record<string, InstlSncnCklstResultSaveDto[]>;
/** 필수첨부파일 목록 */
esntlAtchFileList?: InstlSncnEsntlAtchFileSaveDto[];
/** 검토의견 목록 */
rvwOpnnList?: InstlSncnRvwOpnnSaveRequestDto[];
/** 보완요구 목록 */
splmntDmndList?: InstlSncnSplmntDmndSaveDto[];
}
export type ReviewStepCode = "LOCAL" | "ENV" | "KECO";
/** 설치인가검토의견 응답 DTO */
export type InstlSncnRvwOpnnResponseDto = InstlSncnRvwOpnnSaveRequestDto &
RvwOpnnResponseExtension & {
instlSncnDocNo: string;
rvwOpnnSn: number;
opnrSeComCd: string;
opnrNm: string;
rvwRsltCn: string;
rvwRsltAnmlyYn: string;
atchFileGroupId: string | null;
opnrSeCdNm: string | null;
regDt: string;
};
/** 전자결재 진행상태 (클라이언트 응답용) */
export interface ElaprDocPrgrsClientResponseDto {
elaprDocNo?: string;
elaprDmndId?: string;
elaprPrgrsId?: string;
atrzSeqNo?: number;
atrzSeComCd?: string;
atrzSeComNm?: string;
aprvrUuid?: string;
aprvrUuidNm?: string;
atrzPrgrsSttsComCd?: string;
atrzPrgrsSttsComNm?: string;
rjctRsn?: string | null;
atrzDt?: string | null;
jbpsSeNm?: string;
jbgdSeNm?: string;
ognzNm?: string;
}
// det 응답에 전자결재 진행상태 목록을 포함
export interface InstlSncnMngDetResponse {
detDto: InstlSncnMng;
bizSclList: any[];
bizCstList: any[];
cklstResultList?: any[];
esntlFileList: any[];
etcFileList: any[];
/** 설치인가관리 상세 응답 DTO */
export interface InstlSncnMngDetResponse extends PbadmsMngDetResponseBase<
InstlSncnMngResponseDto,
InstlSncnRvwOpnnResponseDto,
InstlSncnSplmntDmndResponseDto
> {
detDto: InstlSncnMngResponseDto;
bizSclList: BizSclTreeDto[];
bizCstList: BizCstTreeDto[];
esntlFileList: EsntlAtchFileMngTreeResponseDto[];
etcFileList: AtflMngClientResponseDto[];
elaprDocPrgrsList?: ElaprDocPrgrsClientResponseDto[] | null;
}
export interface InstlSncnMngService {
getPage: (params: InstlSncnMngSearchParams) => Promise<SpringPage<InstlSncnMng>>;
getPage: (params: InstlSncnMngSearchParams) => Promise<PageResponse<InstlSncnMngResponseDto>>;
getById: (id: string) => Promise<InstlSncnMngDetResponse>;
save: (payload: InstlSncnMngSaveRequestDto) => Promise<string>;
saveApply: (payload: InstlSncnApplySaveRequestDto) => Promise<string>;
update: (id: string, payload: InstlSncnMngUpdateRequestDto) => Promise<void>;
techReview: (id: string, payload: InstlSncnApplySaveRequestDto) => Promise<void>;
submitSupplement: (id: string, payload: InstlSncnApplySaveRequestDto) => Promise<string>;
submitReqSupplement: (id: string, payload: InstlSncnApplySaveRequestDto) => Promise<string>;
remove: (id: string) => Promise<void>;
changePrgrsSttsComCd: (payload: DocPrgrsUpdateRequestDto) => Promise<void>;
}
+17 -2
View File
@@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import type {
RvwMnpwMngPageSearchDto,
RvwMnpwMngSaveRequestDto,
RvwMnpwMngSearchDto,
RvwMnpwMngUpdateRequestDto,
@@ -10,9 +11,12 @@ import type {
import { rvwMnpwMngService } from "./rvw-mnpw-service";
const rvwMnpwMngBaseKey = [ENDPOINT.AS_COMMON_SERVICE.RVW_MNPW] as const;
const rvwMnpwMngBaseKey = [ENDPOINT.AS_SERVICE.RVW_MNPW] as const;
export function useRvwMnpwMngPage(params: RvwMnpwMngSearchDto, options?: { enabled?: boolean }) {
export function useRvwMnpwMngPage(
params: RvwMnpwMngPageSearchDto,
options?: { enabled?: boolean },
) {
return useQuery({
queryKey: [...rvwMnpwMngBaseKey, "findPage", params],
queryFn: () => rvwMnpwMngService.findPage(params),
@@ -20,6 +24,17 @@ export function useRvwMnpwMngPage(params: RvwMnpwMngSearchDto, options?: { enabl
});
}
export function useRvwMnpwMngLoginUserList(
searchDto: RvwMnpwMngSearchDto,
options?: { enabled?: boolean },
) {
return useQuery({
queryKey: [...rvwMnpwMngBaseKey, "findRvwLoginUserList", searchDto],
queryFn: () => rvwMnpwMngService.findRvwLoginUserList(searchDto),
enabled: options?.enabled,
});
}
export function useRvwMnpwMng(
atrzSeComCd: string | null | undefined,
ognzSn: number | null | undefined,
@@ -3,6 +3,7 @@ import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type {
RvwMnpwMngPageSearchDto,
RvwMnpwMngResponseDto,
RvwMnpwMngSaveRequestDto,
RvwMnpwMngSearchDto,
@@ -10,14 +11,21 @@ import type {
RvwMnpwMngUpdateRequestDto,
} from "../model/rvw-mnpw-interface";
const BASE = ENDPOINT.AS_COMMON_SERVICE.RVW_MNPW;
const BASE = ENDPOINT.AS_SERVICE.RVW_MNPW;
function pkPath(atrzSeComCd: string, ognzSn: number, userUuid: string): string {
return `${BASE}/${atrzSeComCd}/${ognzSn}/${userUuid}`;
async function findPage(
params: RvwMnpwMngPageSearchDto,
): Promise<PageResponse<RvwMnpwMngResponseDto>> {
const response = await client.get<PageResponse<RvwMnpwMngResponseDto>>(BASE, { params });
return response.data;
}
async function findPage(params: RvwMnpwMngSearchDto): Promise<PageResponse<RvwMnpwMngResponseDto>> {
const response = await client.get<PageResponse<RvwMnpwMngResponseDto>>(BASE, { params });
async function findRvwLoginUserList(
searchDto: RvwMnpwMngSearchDto,
): Promise<PageResponse<RvwMnpwMngResponseDto>> {
const response = await client.get<PageResponse<RvwMnpwMngResponseDto>>(`${BASE}/loginuser/me`, {
params: searchDto,
});
return response.data;
}
@@ -26,41 +34,48 @@ async function findById(
ognzSn: number,
userUuid: string,
): Promise<RvwMnpwMngResponseDto> {
const response = await client.get<RvwMnpwMngResponseDto>(pkPath(atrzSeComCd, ognzSn, userUuid));
const response = await client.get<RvwMnpwMngResponseDto>(
`${BASE}/${atrzSeComCd}/${ognzSn}/${userUuid}`,
);
return response.data;
}
async function save(body: RvwMnpwMngSaveRequestDto): Promise<RvwMnpwMngResponseDto> {
const response = await client.post<RvwMnpwMngResponseDto>(BASE, body);
async function saveRvwMnpwMng(body: RvwMnpwMngSaveRequestDto): Promise<RvwMnpwMngResponseDto> {
const { atrzSeComCd, ognzSn, userUuid } = body;
const response = await client.post<RvwMnpwMngResponseDto>(
`${BASE}/${atrzSeComCd}/${ognzSn}/${userUuid}`,
body,
);
return response.data;
}
async function update(
async function updateRvwMnpwMng(
atrzSeComCd: string,
ognzSn: number,
userUuid: string,
body: RvwMnpwMngUpdateRequestDto,
): Promise<RvwMnpwMngResponseDto> {
const response = await client.put<RvwMnpwMngResponseDto>(
pkPath(atrzSeComCd, ognzSn, userUuid),
`${BASE}/${atrzSeComCd}/${ognzSn}/${userUuid}`,
body,
);
return response.data;
}
async function deleteRvwMnpw(
async function deleteRvwMnpwMng(
atrzSeComCd: string,
ognzSn: number,
userUuid: string,
): Promise<string> {
const response = await client.delete<string>(pkPath(atrzSeComCd, ognzSn, userUuid));
const response = await client.delete<string>(`${BASE}/${atrzSeComCd}/${ognzSn}/${userUuid}`);
return response.data;
}
export const rvwMnpwMngService = {
findPage,
findRvwLoginUserList,
findById,
save,
update,
delete: deleteRvwMnpw,
save: saveRvwMnpwMng,
update: updateRvwMnpwMng,
delete: deleteRvwMnpwMng,
} satisfies RvwMnpwMngService;
+2
View File
@@ -1,12 +1,14 @@
export {
useDeleteRvwMnpwMng,
useRvwMnpwMng,
useRvwMnpwMngLoginUserList,
useRvwMnpwMngPage,
useSaveRvwMnpwMng,
useUpdateRvwMnpwMng,
} from "./api/queries";
export { rvwMnpwMngService } from "./api/rvw-mnpw-service";
export type {
RvwMnpwMngPageSearchDto,
RvwMnpwMngResponseDto,
RvwMnpwMngSaveRequestDto,
RvwMnpwMngSearchDto,
@@ -14,13 +14,16 @@ export interface RvwMnpwMngResponseDto {
regDt?: string;
}
/** 백엔드 RvwMnpwMngSearchDto + Pageable */
export interface RvwMnpwMngSearchDto extends PageableParams {
/** 백엔드 RvwMnpwMngSearchDto */
export interface RvwMnpwMngSearchDto {
atrzSeComCd?: string;
ognzSn?: number;
userUuid?: string;
}
/** findPage — RvwMnpwMngSearchDto + Pageable */
export type RvwMnpwMngPageSearchDto = RvwMnpwMngSearchDto & PageableParams;
/** 백엔드 RvwMnpwMngSaveRequestDto */
export interface RvwMnpwMngSaveRequestDto {
atrzSeComCd: string;
@@ -35,7 +38,10 @@ export interface RvwMnpwMngUpdateRequestDto {
/** Controller public 메서드명·시그니처와 동일 */
export interface RvwMnpwMngService {
findPage(params: RvwMnpwMngSearchDto): Promise<PageResponse<RvwMnpwMngResponseDto>>;
findPage(params: RvwMnpwMngPageSearchDto): Promise<PageResponse<RvwMnpwMngResponseDto>>;
findRvwLoginUserList(
searchDto: RvwMnpwMngSearchDto,
): Promise<PageResponse<RvwMnpwMngResponseDto>>;
findById(atrzSeComCd: string, ognzSn: number, userUuid: string): Promise<RvwMnpwMngResponseDto>;
save(body: RvwMnpwMngSaveRequestDto): Promise<RvwMnpwMngResponseDto>;
update(
+1
View File
@@ -0,0 +1 @@
export { handleTusUpload, type TusUploadOptions } from "./lib/utils";
+61
View File
@@ -0,0 +1,61 @@
import { Upload } from "tus-js-client";
import { ENDPOINT, PROXY_HOST } from "@/shared/config";
import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config/env";
import { getCookie } from "@/shared/lib";
interface TusUploadOptions {
metadata?: Record<string, string>;
handleUploadStart?: (upload: Upload) => void;
handleProgress?: (percentage: number) => void;
handleSuccess?: (upload: Upload, atflCd: string, atflId: string) => void;
handleError?: (error: Error) => void;
}
function handleTusUpload(file: File, options?: TusUploadOptions) {
let atflCd = "";
let atflId = "";
const metadata = {
filename: file.name,
filetype: file.type,
...options?.metadata,
};
const upload = new Upload(file, {
endpoint: `${PROXY_HOST}${ENDPOINT.ATTACH_SERVICE.TUS_UPLOAD}`,
chunkSize: 10 * 1024 * 1024,
headers: {
[CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "",
[AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "",
},
metadata,
onError(error) {
console.error("TUS upload failed:", error);
options?.handleError?.(error);
},
onProgress(bytesUploaded, bytesTotal) {
const percentage = (bytesUploaded / bytesTotal) * 100;
options?.handleProgress?.(percentage);
},
onAfterResponse(_req, res) {
const fileInfo = res.getHeader("X-File-Info");
if (!fileInfo) return;
const parsed = JSON.parse(fileInfo) as { atflCd?: string; atflId?: string };
atflCd = parsed.atflCd ?? "";
atflId = parsed.atflId ?? "";
},
onSuccess() {
options?.handleSuccess?.(upload, atflCd, atflId);
},
});
upload.start();
options?.handleUploadStart?.(upload);
return upload;
}
export { handleTusUpload };
export type { TusUploadOptions };
@@ -1 +0,0 @@
export { AcntgCodeManagementPage } from "./ui/acntg-code-management-page";
@@ -1,245 +0,0 @@
import type { AcntgSeMngResponseDto } from "@/entities/acntg";
import type {
AcntgDisplaySlot,
AcntgSeBaseEntry,
AcntgSeGridState,
} from "../model/acntg-se-grid-types";
import type { AcntgSeRow } from "../model/acntg-se-row-types";
import { buildAcntgDisplayRows } from "../model/build-acntg-display-rows";
export function createLoadedRowKey(index: number, acntgSeCd: string): string {
return `loaded-${index}-${acntgSeCd}`;
}
export function createDraftRowKey(): string {
return `draft-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
}
export function createEmptyGridState(): AcntgSeGridState {
return {
baseByKey: {},
structure: [],
drafts: {},
patches: {},
deletedKeys: new Set(),
};
}
function toInitialRow(entry: AcntgSeBaseEntry): AcntgSeRow {
return {
...entry,
isNew: false,
isDirty: false,
isDeleted: false,
};
}
export function buildStructureFromBaseRows(
baseByKey: Record<string, AcntgSeBaseEntry>,
): AcntgDisplaySlot[] {
const rows = Object.values(baseByKey).map(toInitialRow);
const display = buildAcntgDisplayRows(rows);
const cdToRowKey = new Map(rows.map((row) => [row.acntgSeCd, row.rowKey]));
return display.map(({ row, depth }) => ({
rowKey: row.rowKey,
depth,
parentRowKey: row.upAcntgSeCd?.trim() ? (cdToRowKey.get(row.upAcntgSeCd.trim()) ?? null) : null,
}));
}
export function hydrateGridFromServer(content: AcntgSeMngResponseDto[]): AcntgSeGridState {
const baseByKey: Record<string, AcntgSeBaseEntry> = {};
content.forEach((item, index) => {
const rowKey = createLoadedRowKey(index, item.acntgSeCd);
baseByKey[rowKey] = { ...item, rowKey };
});
return {
baseByKey,
structure: buildStructureFromBaseRows(baseByKey),
drafts: {},
patches: {},
deletedKeys: new Set(),
};
}
export function resolveParentAcntgSeCd(
state: AcntgSeGridState,
parentRowKey: string | null,
): string | undefined {
if (!parentRowKey) return undefined;
const parent = mergeAcntgRow(state, parentRowKey);
const cd = parent?.acntgSeCd?.trim();
return cd || undefined;
}
export function mergeAcntgRow(state: AcntgSeGridState, rowKey: string): AcntgSeRow | null {
const draft = state.drafts[rowKey];
if (draft) {
return {
rowKey,
acntgSeCd: draft.acntgSeCd,
acntgSeCdNm: draft.acntgSeCdNm,
expln: draft.expln,
useYn: draft.useYn,
cdLvNo: draft.cdLvNo,
upAcntgSeCd: resolveParentAcntgSeCd(state, draft.parentRowKey),
isNew: true,
isDirty: true,
isDeleted: false,
};
}
const base = state.baseByKey[rowKey];
if (!base) return null;
const patch = state.patches[rowKey] ?? {};
const hasPatch = Object.keys(patch).length > 0;
return {
...base,
...patch,
rowKey,
isNew: false,
isDirty: hasPatch,
isDeleted: state.deletedKeys.has(rowKey),
};
}
export function mergeAllAcntgRows(state: AcntgSeGridState): AcntgSeRow[] {
const keys = new Set([...Object.keys(state.baseByKey), ...Object.keys(state.drafts)]);
return [...keys]
.map((rowKey) => mergeAcntgRow(state, rowKey))
.filter((row): row is AcntgSeRow => row != null);
}
export function hasAcntgGridPendingChanges(state: AcntgSeGridState): boolean {
return (
state.deletedKeys.size > 0 ||
Object.keys(state.drafts).length > 0 ||
Object.keys(state.patches).length > 0
);
}
export function collectDescendantRowKeys(
structure: AcntgDisplaySlot[],
rootRowKey: string,
): Set<string> {
const result = new Set<string>([rootRowKey]);
const queue = [rootRowKey];
while (queue.length > 0) {
const parentKey = queue.shift()!;
for (const slot of structure) {
if (slot.parentRowKey === parentKey && !result.has(slot.rowKey)) {
result.add(slot.rowKey);
queue.push(slot.rowKey);
}
}
}
return result;
}
export function insertRootSlot(
structure: AcntgDisplaySlot[],
slot: AcntgDisplaySlot,
): AcntgDisplaySlot[] {
const firstRootIndex = structure.findIndex((item) => item.parentRowKey === null);
if (firstRootIndex === -1) return [slot, ...structure];
return [...structure.slice(0, firstRootIndex), slot, ...structure.slice(firstRootIndex)];
}
export function insertChildSlot(
structure: AcntgDisplaySlot[],
parentRowKey: string,
slot: AcntgDisplaySlot,
): AcntgDisplaySlot[] {
const parentIndex = structure.findIndex((item) => item.rowKey === parentRowKey);
if (parentIndex === -1) return [...structure, slot];
const insertAt = parentIndex + 1;
return [...structure.slice(0, insertAt), slot, ...structure.slice(insertAt)];
}
export function removeSlotsFromStructure(
structure: AcntgDisplaySlot[],
rowKeysToRemove: Set<string>,
): AcntgDisplaySlot[] {
return structure.filter((slot) => !rowKeysToRemove.has(slot.rowKey));
}
export function patchAcntgGridRow(
state: AcntgSeGridState,
rowKey: string,
patch: Partial<AcntgSeRow>,
): AcntgSeGridState {
const draft = state.drafts[rowKey];
if (draft) {
return {
...state,
drafts: {
...state.drafts,
[rowKey]: {
...draft,
...(patch.acntgSeCd !== undefined ? { acntgSeCd: patch.acntgSeCd } : {}),
...(patch.acntgSeCdNm !== undefined ? { acntgSeCdNm: patch.acntgSeCdNm ?? "" } : {}),
...(patch.expln !== undefined ? { expln: patch.expln ?? "" } : {}),
...(patch.useYn !== undefined ? { useYn: patch.useYn } : {}),
},
},
};
}
const { rowKey: _rk, isNew: _n, isDirty: _d, isDeleted: _del, ...dtoPatch } = patch;
return {
...state,
patches: {
...state.patches,
[rowKey]: { ...state.patches[rowKey], ...dtoPatch },
},
};
}
export function markAcntgRowsDeleted(
state: AcntgSeGridState,
rootRowKey: string,
): AcntgSeGridState {
const keysToDelete = collectDescendantRowKeys(state.structure, rootRowKey);
const deletedKeys = new Set(state.deletedKeys);
for (const key of keysToDelete) {
deletedKeys.add(key);
}
return { ...state, deletedKeys };
}
export function validateDraftParentAcntgSeCd(
state: AcntgSeGridState,
rowKey: string,
): string | null {
const draft = state.drafts[rowKey];
if (!draft?.parentRowKey) return null;
const parent = mergeAcntgRow(state, draft.parentRowKey);
if (!parent?.acntgSeCd?.trim()) {
return "하위 항목을 추가하려면 상위 코드를 먼저 입력하세요.";
}
return null;
}
export function removeDraftFromGrid(state: AcntgSeGridState, rowKey: string): AcntgSeGridState {
const { [rowKey]: _removed, ...restDrafts } = state.drafts;
const keysToRemove = collectDescendantRowKeys(state.structure, rowKey);
return {
...state,
drafts: restDrafts,
structure: removeSlotsFromStructure(state.structure, keysToRemove),
};
}
@@ -1,62 +0,0 @@
import { ACNTG_SE_SAVE_MODE, type AcntgSeMngSaveRequestDto } from "@/entities/acntg";
import type { AcntgSeRow } from "../model/acntg-se-row-types";
import { getVisibleRows } from "./row-state";
function compareCdLvNoDesc(a: AcntgSeRow, b: AcntgSeRow): number {
return (b.cdLvNo ?? 1) - (a.cdLvNo ?? 1);
}
function compareCdLvNoAsc(a: AcntgSeRow, b: AcntgSeRow): number {
return (a.cdLvNo ?? 1) - (b.cdLvNo ?? 1);
}
export function buildSaveMultiPayload(rows: AcntgSeRow[]): AcntgSeMngSaveRequestDto[] {
const payload: AcntgSeMngSaveRequestDto[] = [];
const deletedRows = rows.filter((row) => row.isDeleted && !row.isNew).sort(compareCdLvNoDesc);
for (const row of deletedRows) {
payload.push({
mode: ACNTG_SE_SAVE_MODE.DELETE,
acntgSeCd: row.acntgSeCd,
acntgSeCdNm: row.acntgSeCdNm?.trim() || "",
upAcntgSeCd: row.upAcntgSeCd,
cdLvNo: row.cdLvNo,
useYn: row.useYn,
expln: row.expln?.trim() || undefined,
});
}
const visibleRows = getVisibleRows(rows).sort(compareCdLvNoAsc);
for (const row of visibleRows) {
if (row.isNew) {
payload.push({
mode: ACNTG_SE_SAVE_MODE.CREATE,
acntgSeCd: row.acntgSeCd.trim(),
acntgSeCdNm: row.acntgSeCdNm.trim(),
upAcntgSeCd: row.upAcntgSeCd,
cdLvNo: row.cdLvNo,
useYn: row.useYn,
expln: row.expln?.trim() || undefined,
});
continue;
}
if (row.isDirty) {
payload.push({
mode: ACNTG_SE_SAVE_MODE.UPDATE,
acntgSeCd: row.acntgSeCd.trim(),
acntgSeCdNm: row.acntgSeCdNm.trim(),
upAcntgSeCd: row.upAcntgSeCd,
cdLvNo: row.cdLvNo,
useYn: row.useYn,
expln: row.expln?.trim() || undefined,
});
}
}
return payload;
}
@@ -1,5 +0,0 @@
import type { AcntgSeRow } from "../model/acntg-se-row-types";
export function getVisibleRows(rows: AcntgSeRow[]): AcntgSeRow[] {
return rows.filter((row) => !row.isDeleted);
}

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