Compare commits

...
Author SHA1 Message Date
100JM 03a08a320a [ST] - fix: DB 표준화에 따른 소스 수정 2026-10-14 14:24:03 +09:00
rsone001 f2ec9327fd feat: [ST] - eval 제거 2026-09-30 17:17:01 +09:00
yangsein 5331e10c96 [refactor] 대시보드 - 통계처리현황 반려되어 제출중인 상태 구분 처리 포함 2026-09-30 16:59:05 +09:00
rsone001 de8971bea6 [ST] fix: 대시보드 통계 처리 현황 panel css 수정 2026-09-08 13:43:26 +09:00
rsone001 a7fd6a8e16 [ST] - fix: 취약점 점검 관련 조치 2026-09-07 14:26:00 +09:00
rsone001 8039ab8281 [ST] - refactor: 2-5 맨홀 지표 변경된 테이블에 맞게 수정, 일반검증 항목 입력기간일 시 등록/수정 불가 2026-09-04 16:05:26 +09:00
rsone001 794691f05e [ST] - fix: 제출결과(검증)화면에 시설명 컬럼 추가 2026-09-03 16:29:40 +09:00
rsone001 e58b84a01b [ST] - fix: input 제출 - 시계열 검증 요청 response & request 수정, 대시보드 구성 수정 2026-09-03 15:27:19 +09:00
rsone001 3d51f003c0 Merge remote-tracking branch 'origin/feature/sein' into iipbs/st/dev 2026-09-03 13:52:34 +09:00
yangsein b5a99e5f59 [feat] 대쉬 보드 일정관리 제거 , 통계 현황상태 추가 2026-09-03 13:44:09 +09:00
rsone001 afca685fd4 [ST] - fix: 2-3 매설연장 관리자일시 수정 가능하도록 적용, SearchFrom 로직 수정 2026-09-03 11:26:23 +09:00
rsone001 261941452a [ST] fix - 2-2 지표 컬럼 설정 및 저장 제출 로직 수정 2026-09-02 16:41:12 +09:00
rsone001 8576ef83b5 [ST] - refactor: 6-1 중심처리시설 row는 위탁계약기간 직접 수정 못하도록 적용 2026-09-01 16:25:55 +09:00
rsone001 e80b8953a0 [ST] - fix: 일반검증 수식 사용자 직접입력 등록시 공백제거 2026-09-01 14:41:49 +09:00
rsone001 e8b309460e [ST] input 특광역시 로직 수정, 일반검증 엑셀 다운로드 추가, 모든 number 타입 컬럼에 footer 추가 2026-08-28 17:08:20 +09:00
chanii8 37ac927c45 [st] feat: 시계열 검토 이력 > 지자체 진입 시 시도/시군구 자동 선택 2026-08-28 17:05:40 +09:00
chanii8 bf9a34cbe8 [st] feat: 하수도 통계 검증 > 시도/시군구 셀렉트에 '전체' 옵션 추가 2026-08-28 15:55:05 +09:00
rsone001 18a11ab5fa [ST] - refactor: 입력화면 전용 시도 조회 api 추가 2026-08-27 17:22:34 +09:00
rsone001 214ff1a435 [ST] - fix: 일반 검증 타입 총계 검증일 시 group by 항목 렌더링 되지 않게 수정 2026-08-27 15:44:45 +09:00
rsone001 d7cc579335 [ST] - refactor: 지난검증결과 불러오기 필드 추가(시설명) 2026-08-27 15:32:12 +09:00
rsone001 9b53c1ae08 [ST] - fix: 일반검증 항목 관리, 참조 지표(컬럼) 오류 로직 수정 2026-08-27 13:07:33 +09:00
rsone001 64ed53cf4d [ST] refactor: input - 자동 계산 cell 부동소수점 문제 해결을 위해 decimal.js 라이브러리 추가 및 계산 로직 수정, 6-2 지표 로직 수정 2026-08-25 16:04:31 +09:00
rsone001 043fd3d4a8 [ST] - 입력 6-1 공공하수처리시설 컬럼 설정 변경 2026-08-24 17:19:33 +09:00
rsone001 6dd09971f1 [ST] - 컬럼 자동 계산식 오류 수정 2026-08-24 16:39:24 +09:00
chanii8 23424b5eee [st] chore: 통계 검증 리포트 초기 조회 파라미터 수정 2026-08-24 14:09:12 +09:00
chanii8 3f190dfff6 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-08-21 17:27:37 +09:00
chanii8 6cc02da7a4 [st] feat: 통계 검증 리포트 목록 조회 API 수정 2026-08-21 17:27:30 +09:00
rsone001 c87ccf8788 [ST] - fix: 입력 저장&제출시 빈값 null처리 2026-08-21 14:56:34 +09:00
rsone001 8e757ab9db Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-08-20 16:31:13 +09:00
rsone001 de7e66afb4 [ST] - fix: 11-4 데이터 체크 수정 2026-08-20 16:31:00 +09:00
chanii8 c981b83284 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-08-20 13:07:50 +09:00
chanii8 927bb7cb3a [st] chore: eslint 해결 2026-08-20 11:48:18 +09:00
chanii8 725976b5c0 [st] feat: 시도/시군구 조회 쿼리 변경 2026-08-20 11:47:50 +09:00
rsone001 cdbe6a855f [ST] - fix: 검토 하수관로 컬럼 수정 및 엑셀 내보내기 셀 스타일 수정 2026-08-19 12:31:37 +09:00
chanii8 a1c96e9e98 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-08-19 11:58:06 +09:00
chanii8 1488805242 [as] feat: 통계 검증 리포트 새로고침 버튼 추가 2026-08-19 11:57:58 +09:00
rsone001 5b12f81b8f [ST] - fix: 통계 집계표 조회 기본값 수정, 엑셀 다운로드 스타일 적용, 요약표 마우스 툴팁 추가 / 입력단 오류 수정 및 검토단 컬럼 설정 수정 2026-08-18 17:44:22 +09:00
rsone001 df24e7c2a8 [ST] - fix: 일반 검증 수식 안내 문구 수정 2026-08-13 16:20:54 +09:00
rsone001 3b5fee047b [ST] - fix: qa 조치, 일반 검증 개발 수정 2026-08-13 15:30:00 +09:00
rsone001 181730bcde [ST] - fix: 입력: 새로고침 시 열려있는 모든 텝의 공통 api가 실행되는 현상 수정, 하수도보급률 저장&제출 request 수정(외국인총인구 제거, 총면적 추가), 검증 툴팁 max-width 추가, 지난 검증 결과 및 제출 결과 데이터 마우스 툴팁 추가 2026-08-10 17:58:05 +09:00
chanii8 678af2f06e [st] chore: 승인 정보 입력 폼 수정 2026-08-10 15:48:19 +09:00
rsone001 82d67bb4c6 [ST] - fix: 검증 항목 명칭 변경 2026-08-07 17:22:08 +09:00
rsone001 85ef1805e5 [ST] - 공단 오류 및 개선 요청사항 처리 2026-08-07 16:24:39 +09:00
chanii8 26f64988ef [st] typo: 404 페이지 문구 수정 2026-08-06 17:06:00 +09:00
chanii8 29e39f98af Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-08-06 15:15:09 +09:00
chanii8 5192f3e3c0 [st] feat: 404 페이지 구현 2026-08-06 15:15:04 +09:00
rsone001 83de317308 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-08-05 10:09:58 +09:00
rsone001 84b1fd07ae [ST] - management service code refresh 로직 추가 2026-08-05 10:08:26 +09:00
chanii8 a88f8ab33e [st] feat: 로그아웃 라우트 핸들러 적용 2026-08-03 15:56:48 +09:00
chanii8 c67b5844b6 [st] chore: metadata 수정 2026-08-03 11:14:48 +09:00
chanii8 3c65e6f8f5 chore: css 모듈 처리 2026-08-03 10:43:17 +09:00
chanii8 1efe2dbe89 remove: ClientIpGuard 제거 2026-07-31 17:56:21 +09:00
chanii8 36e56dcaef Merge branch 'iipbs/st/dev' 2026-07-30 11:28:08 +09:00
chanii8 e42e28d062 [st] chore: 통계검증리포트 > 승인정보입력 다이얼로그 수정 2026-07-30 11:27:36 +09:00
chanii8 0786053022 Merge remote-tracking branch 'origin/iipbs/st/dev' 2026-07-30 11:24:13 +09:00
rsone001 791ec1ae2d [ST] - code-refresh 로직 추가 2026-07-30 11:01:02 +09:00
rsone001 4e4942313a [ST] - fix: 통계일정관리 불필요 컬럼 제거 2026-07-28 11:34:46 +09:00
chanii8 e6fcbfbde4 chore: tsserver 용량 관리 2026-07-24 16:25:55 +09:00
chanii8 ed4d1b73cb refactor: X-Device-Id 공통 로직으로 변경 2026-07-24 15:53:23 +09:00
chanii8 e3dac52d7e Merge branch 'iipbs/st/dev' 2026-07-24 10:56:09 +09:00
chanii8 d1d8a1eff3 [st] feat: 통계 검증 리포트 시도/시군구 셀렉트 로직 수정 2026-07-24 10:47:57 +09:00
jcchoi 4502d55ce1 Merge remote-tracking branch 'origin/iipbs/st/dev' 2026-07-23 20:49:31 +09:00
rsone001 3d3b978070 [st] - fix: 검토이력조회 request 누락 수정 2026-07-23 16:36:29 +09:00
rsone001 117082c862 [st] - fix: 입력 메뉴 데이터 항목 추가 시 해당사항없음 false로 갱신되지 않던 오류 수정 2026-07-23 14:26:11 +09:00
chanii8 bfa03aabc0 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-07-23 11:43:30 +09:00
chanii8 f535cd1010 [st] feat: 대시보드 일정안내 empty 구현 및 일정관리 월 이동 구현 2026-07-23 11:33:21 +09:00
chanii8 2f6d4ebec1 chore: 코드 정리 2026-07-22 14:13:38 +09:00
rsone001 1c3cc5b61b Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-07-22 13:39:57 +09:00
rsone001 db7584cb01 [st] - fix: 집계06.공공하수처리시설 조회 조건 추가, 입력06.공공하수처리시설 매핑 계약기간 DatePicker 수정, 입력자 예외 제출 valuedation 수정 2026-07-22 13:38:58 +09:00
chanii8 b842408f41 Merge remote-tracking branch 'origin/iipbs/as/dev' 2026-07-22 11:36:12 +09:00
chanii8 6e33a62ed9 [as] feat: 설계자문 페이지 구현 2026-07-22 11:34:38 +09:00
mind-one005 36231a5eca Merge branch 'iipbs/wr/dev' 2026-07-22 09:23:43 +09:00
mind-one005 b41a317788 wr: fo: sn: 지도화면 session confirm 2026-07-21 17:59:15 +09:00
chanii8 84fb81b245 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/as/dev 2026-07-21 15:56:50 +09:00
chanii8 e18f759fab [as] feat: 설계자문 엔티티 생성 2026-07-21 14:32:14 +09:00
chanii8 ce9c7b17a2 Merge remote-tracking branch 'origin/iipbs/st/dev' 2026-07-21 13:54:32 +09:00
chanii8 c2d6f7fe37 Merge remote-tracking branch 'origin/iipbs/cs/dev' 2026-07-21 13:54:28 +09:00
chanii8 9b18aa9cef Merge remote-tracking branch 'origin/iipbs/cm/dev' 2026-07-21 13:54:22 +09:00
chanii8 5eccb58aa9 Merge remote-tracking branch 'origin/iipbs/as/dev' 2026-07-21 13:54:16 +09:00
chanii8 acce1364fa Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-07-21 13:53:30 +09:00
chanii8 625c5f5fe2 [st] chore: 토큰 자동 리프레시 코드 주석 처리 2026-07-21 13:53:24 +09:00
chanii8 dffa9d74a0 [cs] chore: 토큰 자동 리프레시 코드 주석 처리 2026-07-21 13:52:59 +09:00
chanii8 668a8ef844 [cm] chore: 토큰 자동 리프레시 코드 주석 처리 2026-07-21 13:52:26 +09:00
chanii8 df4d708981 [as] chore: 토큰 자동 리프레시 코드 주석 처리 2026-07-21 13:51:10 +09:00
chanii8 c48a46c7ab feat: 토큰 시간 만료 시 authService.logout 함수 실행 2026-07-21 13:50:19 +09:00
chanii8 da844fb3a7 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-07-21 11:50:36 +09:00
chanii8 3e1f89d90f [common/components] chore: 로그인 연장 콘솔 추가(임시) 2026-07-21 11:47:39 +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 aaf898fb4c Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/cm/dev 2026-07-20 17:47:19 +09:00
rsone001 533e89c919 [st] - fix: 이상치선별보고서 css 수정 2026-07-20 17:11:10 +09:00
chanii8 2f0448a98e [common/components] refactor: banner, menu, site를 기본 fetch 로 수정 2026-07-20 17:08:36 +09:00
chanii8 aab8cf9622 [cm] feat: 인증 서비스에 "X-Device-Id" 적용 2026-07-20 15:37:17 +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
mind-one005 c0888ea2a4 wr: 빗물이용시설 엑셀다운 수정 2026-06-26 11:30:57 +09:00
kiminchan 0fe1aa983c 지반침하 반경 내 관로정보 요약 표시 개선 2026-06-26 10:35:40 +09:00
kiminchan 180eb1f59d 관로 이동 도구 on/off 유지 및 입력정보 조회 오류 방어 2026-06-26 10:15:24 +09:00
mind-one005 1ead18b53c wr: 지도팝업 작업계획 2026-06-26 10:01:10 +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
mind-one005 07db5b1fbd wr: 페이징 수정 2026-06-25 18:09:04 +09:00
rsone001 162e18142c [st] - 입력단 작업 완료 2026-06-25 18:02:31 +09:00
mind-one005 6110abebb4 wr: 페이징 오류 작업계획 2026-06-25 16:30:29 +09:00
mind-one005 05e9082831 wr: black 테마 수정 2026-06-25 15:23:04 +09:00
mind-one005 76a634cf9a fo: 테마적용1 2026-06-25 14:34:55 +09:00
kiminchan d4797617a6 정보검색 팝업 초기 코드 로딩 및 최초 조회 보완 2026-06-25 10:40:28 +09:00
kiminchan be67395f13 정보검색 팝업 조회조건 공통코드 초기화 2026-06-25 10:22:40 +09:00
mind-one005 793c673414 wr: 페이지 내 색상 정의 수정2 2026-06-24 18:16:22 +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
mind-one005 2a76c279eb wr: 페이지 내 색상 정의 수정1 2026-06-24 16:38:53 +09:00
kiminchan 2767260505 지반침하 레이어 아이콘 PNG 재인코딩 2026-06-24 16:13:52 +09:00
kiminchan bba4a8eeba 시설물 레이어 패널 토글 복구 2026-06-24 15:39:06 +09:00
mind-one005 5d1ba3fd87 wr: theme 적용1 2026-06-24 15:13:16 +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
mind-one005 99db9a0b77 wr: business_component css 수정1 2026-06-23 22:05:19 +09:00
mind-one005 4126bc2234 wr: 작업계획 2026-06-23 13:32:25 +09:00
mind-one005 d4f1ec095e Merge branch 'iipbs/wr/dev' into iipbs/dm/dev 2026-06-23 11:42:50 +09:00
LEEKYUHYUN bbf3ce3948 wr: 테이블 헤더 복구 2026-06-23 10:38:05 +09:00
rsone001 46cf24c193 [st] - 입력단 기능 작업중, 집계 요약표 다운로드 2026-06-23 10:20:20 +09:00
LEEKYUHYUN fb5b8356c7 물 재이용 소개 이미지 교체 2026-06-22 18:09:16 +09:00
LEEKYUHYUN 1333bd040a 물 재이용 소개 이미지 교체 2026-06-22 18:02:57 +09:00
LEEKYUHYUN 013c07ef59 Merge branch 'iipbs/dm/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/dm/dev 2026-06-22 17:55:20 +09:00
mind-one005 29fd59a321 wr: 수요처 readonly 처리 2026-06-22 17:42:10 +09:00
mind-one005 3bc152c341 dm: 용어수정 2026-06-22 16:35:19 +09:00
LEEKYUHYUN ea274f8f7d 랜딩 페이지 이미지 교체 2026-06-22 16:33:16 +09:00
LGS 7ac53e56d7 WR dashboard-black theme edit-Leeinseo 2026-06-22 13:17:12 +09:00
LEEKYUHYUN 7b1c12e952 블루 테마 카드 스타일 수정_FO 2026-06-22 13:10:50 +09:00
LGS 17dafe6530 Merge branch 'iipbs/dm/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/dm/dev 2026-06-22 13:09:57 +09:00
LGS ef331363f7 Revert "[ wr ] 물재이용 매칭시스템 Black 테마 css 추가"
This reverts commit 4f10864c2c.
2026-06-22 13:09:53 +09:00
LGS 4f10864c2c [ wr ] 물재이용 매칭시스템 Black 테마 css 추가 2026-06-22 13:09:20 +09:00
Leeinseo 4b5ceb0e0a FO dashboard-red theme edit-Leeinseo 2026-06-22 13:06:30 +09:00
mind-one005 4fa50161fd wr: width 조정 2026-06-22 09:43:09 +09:00
mind-one005 3d27e07ccb fo: theme css 추가 2026-06-19 17:27:30 +09:00
mind-one005 ca1ca71a62 Merge branch 'iipbs/wr/dev' into iipbs/dm/dev 2026-06-19 16:45:02 +09:00
mind-one005 bdd6c7b804 dm: theme css 추가 2026-06-19 16:44:30 +09:00
mind-one005 04b8082f04 wr: width 고정 2026-06-19 16:15:48 +09:00
yangsein 82ab1376cc feat:행정동 탭코드 추가 2026-06-18 15:38:10 +09:00
kiminchan 2c27a67cc3 위성지도 기능 개선 및 침수 레이어 범례 보완 2026-06-18 11:03:39 +09:00
mind-one005 3451b19b96 wr: next-env.d.ts 2026-06-18 10:59:41 +09:00
mind-one005 ae7c1ea1c4 Merge branch 'iipbs/wr/dev' 2026-06-18 10:47:12 +09:00
mind-one005 9c76b955e2 wr: 오류 수정 2026-06-18 10:35:10 +09:00
mind-one005 6c4bfe2798 wr: 수요처 매칭 검토 2026-06-18 10:27:00 +09:00
mind-one005 98189cd14a dm: 총대장균군수 단위 수정 2026-06-17 17:25:18 +09:00
mind-one005 823ce262bb wr: 수요처 매칭 검토 천단위 , 건수 10건 2026-06-17 16:29:28 +09:00
mind-one005 34358400e8 wr: 지도점검 현황 팝업 table 항목명 수정 2026-06-17 14:58:41 +09:00
mind-one005 da9d069a9a wr: 총대장균군수 단위 수정 외 2026-06-17 13:32:23 +09:00
mind-one005 58a03678e4 wr: 시설명 자동검색 제거 2026-06-16 18:16:14 +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
mind-one005 f5c281cf86 wr: 시군구 해당 위치로 2026-06-16 15:51:44 +09:00
chanii8 c72bcea7d9 Merge branch 'iipbs/as/dev'
Deploy Frontend / deploy (push) Successful in 1m1s
2026-06-16 15:05:15 +09:00
chanii8 49cd160e76 Merge branch 'iipbs/cs/dev'
Deploy Frontend / deploy (push) Successful in 1m2s
2026-06-15 17:43:18 +09:00
chanii8 5ca93a25ca [cs] chore: 상위코드 목록 컬럼 수정 2026-06-15 17:42:55 +09:00
kiminchan 2ea207a63a 통수위험 팝업 디자인 보완 2026-06-15 17:34:24 +09:00
chanii8 55554fc781 Merge branch 'iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 3m49s
2026-06-15 16:43:55 +09:00
chanii8 8e78c10304 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-06-15 16:43:35 +09:00
chanii8 0eb74b46ba [st] feat: 메뉴 경로에 '/cms' 포함 시 탭 로직 예외 처리 2026-06-15 16:42:38 +09:00
rsone001 f21dde8f7d [st] - tab & menu 로직 수정 2026-06-15 16:34:02 +09:00
kiminchan e47c00f0b6 침수흔적도 목록/지도 상세 팝업 mock 구현 및 md 업데이트 2026-06-15 16:28:37 +09:00
mind-one005 1fc6707bb3 wr: 총대장균군수 단위 수정 2026-06-15 16:27:41 +09:00
rsone001 44c5d66afa Merge remote-tracking branch 'origin/feature/sein' into iipbs/st/dev 2026-06-15 15:54:04 +09:00
mind-one005 04edb49c9f wr: 지도 back 테마 적용 2026-06-15 15:48:05 +09:00
yangsein 7483d351c3 Merge remote-tracking branch 'origin/iipbs/st/dev' into feature/sein 2026-06-15 14:53:01 +09:00
rsone001 e3eb542e59 [st] - 입력단 메뉴 작업중 2026-06-15 10:38:33 +09:00
yangsein 428b199dd3 feat:12하수및분뇨관리사업 2026-06-15 01:02:13 +09:00
yangsein 3c4a6efeea feat:10.물재이용 2026-06-15 01:01:51 +09:00
yangsein dae7024043 feat:[집계표]13.하수도 서비스수준 2026-06-15 01:00:31 +09:00
yangsein 64e5c26e8c feat: [집계표] 12.하수및 분뇨관리사업 2026-06-15 00:59:03 +09:00
yangsein 84ae60edd6 feat:[집계표] 11하수및 분뇨찌꺼기 2026-06-15 00:57:39 +09:00
yangsein d4c9989069 feat: [집계표] 물재이용 2026-06-15 00:56:20 +09:00
yangsein 6cda413913 feat:[집계표]09.하수도 재정 2026-06-15 00:55:06 +09:00
yangsein a11eaf031d feat:[집계표]08.개인하수처리시설 2026-06-15 00:52:54 +09:00
yangsein 3c701359fd feat:[집계표]분뇨처리시설 2026-06-15 00:50:54 +09:00
yangsein 85ba9fe0ab feat:[집계표]05 펌프장 2026-06-15 00:48:45 +09:00
yangsein 71077f4c8e feat:[집계표]04.하수저류시설 및 빗물 펌프장 2026-06-15 00:47:11 +09:00
yangsein 4c7ff53d78 feat:[집계표]03.하수관로 유지관리 2026-06-15 00:43:03 +09:00
changwng bdc3a94ef1 [as frontend] 결제자 지정및 결제 요청 테이블에 넣는 로직 추가 2026-06-12 13:26:41 +09:00
kiminchan 83a3810301 시설물 레이어 ui 변경 작업 2026-06-12 13:26:35 +09:00
mind-one005 b31691dd9c dm: 물재이용 소개, 사례 id 수정 2026-06-12 10:33:30 +09:00
changwng 0647df733a [as frontend] 결제 관련 로직 추가 2026-06-11 19:00:22 +09:00
mind-one005 0131cae0d7 wr: 하수재이용시설등록 후 목록 재조회 2026-06-11 17:45:58 +09:00
chanii8 f37b19eab6 Merge branch 'iipbs/cs/dev'
Deploy Frontend / deploy (push) Successful in 5m3s
2026-06-11 16:38:24 +09:00
chanii8 7c1db5e0e0 [cs] refactor: useSearchParamsState 생성 및 적용 2026-06-11 16:37:15 +09:00
kiminchan c6309bf14d 지반침하정보 총 결과 건수 표시 2026-06-11 11:36:13 +09:00
mind-one005 2c495ab8fd wr: help 이미지 수정 2026-06-11 09:27:25 +09:00
dongsilove ced1b7084d wr: refresh token 원복 2026-06-10 23:22:44 +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
mind-one005 b0c871eff7 wr: 하수재이용시설 등록 수정 2026-06-10 15:30:37 +09:00
changwng 4148a87f32 [as frontend] 시도/구군 추가 2026-06-10 14:16:43 +09:00
mind-one005 bd83fa3417 Merge remote-tracking branch 'origin/main' into iipbs/wr/dev 2026-06-10 11:36:11 +09:00
mind-one005 c0b63a655e wr: isReset 삭제 2026-06-10 11:35:09 +09:00
mind-one005 cc21aaede1 wr: 하수재이용시설현황 속도 개선 2026-06-10 11:16:10 +09:00
jcchoi 0c71c80a25 대시보드 알림림 스크롤 처리 2026-06-09 17:29:50 +09:00
kiminchan 823d1d7812 지반침하 연도 조건과 팝업 조회조건 동기화 정리 2026-06-09 16:42:15 +09:00
kiminchan a274b5a261 지반침하 팝업 조회조건, 다운로드, 지도표출 방식 정리 2026-06-09 15:26:09 +09:00
rsone001 49e42757bc Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-06-09 10:34:45 +09:00
chanii8 0964f442be Merge branch 'iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 2m48s
2026-06-09 10:08:45 +09:00
chanii8 73ceeb1b38 [st] feat: 대시보드 공지사항 api 수정 2026-06-09 10:08:00 +09:00
changwng 06f68ec392 Merge remote-tracking branch 'origin/iipbs/as/dev' into iipbs/as/dev 2026-06-08 18:21:40 +09:00
changwng e5dc347276 [as frontend] 시도, 군구 추가 2026-06-08 18:21:35 +09:00
changwng 7c6c43b334 [as frontend] 시도, 군구 추가 2026-06-08 18:06:01 +09:00
kiminchan 8737d1eb0a gis 편집 스냅핑, 꼭지점 제거 기능 구현 2026-06-08 17:40:10 +09:00
chanii8 abf53afafc [as] chore: 검토인력관리 사용자명에 직급 텍스트 추가 2026-06-08 17:04:38 +09:00
chanii8 fc70963fa9 [as] feat: 검토인력 인터페이스 수정 & 결재선관리 페이지 수정 2026-06-08 16:18:42 +09:00
mind-one005 814365d7de wr: common-codes error 메시지 수정 2026-06-08 11:44:12 +09:00
mind-one005 8c1a976b5b wr: isInit 2026-06-08 11:02:11 +09:00
mind-one005 66db81071e wr: 빌드오류 해결 2026-06-08 09:06:58 +09:00
mind-one005 6df6e612bc wr: 공통코드 조회 수정 2026-06-05 17:44:51 +09:00
chanii8 84013993c9 [as] feat: 공종코드 및 회계코드 하위코드 추가 방식 변경
Deploy Frontend / deploy (push) Successful in 9m46s
2026-06-05 17:18:14 +09:00
kiminchan a123588e32 지반침하정보 지도, 상세, 설정, 목록 기능 구현 2026-06-05 16:30:58 +09:00
mind-one005 3fee2a781f wr: 하수재이용시설등록 수정 2026-06-05 16:15:46 +09:00
chanii8 acaf2dd9f4 [as] feat: 검토인력관리 페이지 구현 2026-06-05 14:32:14 +09:00
rsone001 55839f0913 Merge branch 'main' into iipbs/st/dev 2026-06-05 10:40:29 +09:00
rsone001 f68131fc13 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-06-05 10:39:50 +09:00
rsone001 7b9081a125 [st] - feat: 입력 04 작업 중 & 수정 요청 사항 적용 2026-06-05 10:35:31 +09:00
chanii8 0947b76a6e Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/as/dev 2026-06-05 10:33:30 +09:00
chanii8 23c18da399 next 버전 업그레이드
> [2026 security release](https://vercel.com/changelog/next-js-may-2026-security-release)
2026-06-05 10:29:01 +09:00
chanii8 ec18f17c79 [as] feat: 검토인력관리 서비스 추가 2026-06-05 10:22:25 +09:00
chanii8 cdfa1e56e5 [as] chore: 버튼 색상 변경 2026-06-05 09:34:22 +09:00
chanii8 434c3cfc08 Merge branch 'iipbs/as/dev'
Deploy Frontend / deploy (push) Successful in 6m20s
2026-06-04 17:49:42 +09:00
chanii8 aee3449578 [as] feat: 공종코드관리, 회계코드관리 페이지 구현 2026-06-04 17:49:00 +09:00
mind-one005 54d3a35b38 wr: store code 조회 위치 변경 2026-06-04 15:56:28 +09:00
chanii8 b640fffd0d Merge branch 'iipbs/cs/dev' 2026-06-04 15:56:27 +09:00
chanii8 981e4aab54 Merge branch 'iipbs/as/dev' 2026-06-04 15:56:06 +09:00
chanii8 6b68894d23 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-06-04 15:54:46 +09:00
kiminchan cc91225b01 관로 분석 재질별, 경과년도&재질별 조회 오류 해결 2026-06-04 15:54:32 +09:00
chanii8 97a8b2811e [cs] feat: 회원관리 폼 수정 2026-06-04 15:54:30 +09:00
kiminchan af8db9457f 노후관로 팝업 위치 수정 2026-06-04 15:16:53 +09:00
chanii8 2c3839fc5f Merge branch 'iipbs/as/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/as/dev 2026-06-04 14:43:12 +09:00
changwng e9caa1b84d [as frontend] tech 리뷰 처리 부분 수정 2026-06-04 13:27:09 +09:00
kiminchan 5860cf3a22 Merge remote-tracking branch 'origin/main' into iipbs/sn/dev_kic 2026-06-04 13:08:00 +09:00
chanii8 d2d5502f8d [as] refactor: 코드관리 -> 공종코드관리/회계코드관리 분리 2026-06-04 12:00:47 +09:00
jcchoi 77f2546cc4 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-06-04 11:49:18 +09:00
jcchoi 4df353518f 공통코드(AUTHSECD) 변경에 따른 수정 2026-06-04 11:49:09 +09:00
chanii8 ee114dec7e Merge branch 'iipbs/st/dev' 2026-06-04 11:12:08 +09:00
chanii8 d58b2db786 [st] chore: 검증항목관리 그리드 컬럼 수정 2026-06-04 11:11:36 +09:00
mind-one005 1c2eaef5a4 Merge branch 'iipbs/wr/dev' 2026-06-04 11:06:27 +09:00
changwng bb7cce47ac [as frontend] 체크리스트 뷰시 rvw_stp_com_cd.리뷰단계코드및, 업무구분별로 뷰단에서 보이는 부분정리 2026-06-04 11:02:43 +09:00
mind-one005 ba910d6bfe wr: refresh token관련 수정 2026-06-04 09:54:41 +09:00
changwng 2a8c2ae542 [as frontend] 체크리스트 입력 저장시 validation 및 추가로 서버 저장로직으로 처리 2026-06-02 19:10:56 +09:00
changwng 56f52194a6 [as frontend] 체크리스트 입력 3개* 단계별 입력 *3개 전부 표시되도록 화면 수정 2026-06-02 18:08:48 +09:00
mind-one005 a71b20ba6f wr: refresh token관련 2026-06-02 18:01:48 +09:00
changwng b2282accb1 [as frontend] taskSeComCd 다중체크 처리 로직 구현 2026-06-02 17:01:33 +09:00
mind-one005 5def2ec5ee wr: .env.local 2026-06-02 16:39:40 +09:00
kiminchan 9d6cc405a9 하수관망도 - 처리구역분석 상세정보 매설년도-> 경과년도 변경 2026-06-02 16:29:07 +09:00
kiminchan 1c37591bab 노후관로 경과년도 표시 조건 보완 2026-06-02 15:44:36 +09:00
changwng 9a8350dae8 [as frontend] 체크리스트 조회시 항목 선택 체크 2026-06-02 13:25:06 +09:00
changwng 77fd719f96 [as frontend] 체크리스트 조회시 cklstResultList로 리스트로 처리 하도록 수정 2026-06-02 13:17:09 +09:00
changwng aa831141f6 [as frontend] 체크리스트 항목 뷰에 추가 처리 2026-06-02 11:55:40 +09:00
changwng fb148a77c1 [as frontend] 체크 리스트 표시 여부 적용 2026-06-02 11:30:30 +09:00
changwng d8d98aee6d [as frontend] 공통 수정로직 변경 처리 2026-06-01 18:05:03 +09:00
changwng 87f9b90dbf Merge remote-tracking branch 'origin/iipbs/as/dev' into iipbs/as/dev
# Conflicts:
#	apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx
2026-06-01 17:23:38 +09:00
changwng b704db99b1 [st frontend] 저장시 picRvwCn 검토 내용을 추가 저장 로직 2026-06-01 17:22:32 +09:00
chanii8 f231564548 Merge branch 'iipbs/as/dev'
Deploy Frontend / deploy (push) Successful in 2m23s
2026-06-01 16:54:59 +09:00
chanii8 db04b13310 [as] chore: 체크리스트관리 페이지 수정 2026-06-01 16:50:42 +09:00
chanii8 9958ea72b8 [as] feat: 코드관리 페이지 구현 2026-06-01 16:41:18 +09:00
mind-one005 35f765d870 wr: 하수재이용시설 현황 페이징 수정 2026-06-01 16:31:49 +09:00
chanii8 b6afaba02a [as] feat: 공종코드 & 회계코드 서비스 구현 2026-06-01 16:07:04 +09:00
mind-one001 c7c69ea5a7 gis 편집 2026-06-01 15:49:50 +09:00
changwng 9b9edec627 Merge remote-tracking branch 'origin/iipbs/as/dev' into iipbs/as/dev 2026-06-01 14:18:35 +09:00
changwng d4302550b4 [st frontend] 조회시 체크리스트 3개 부분 조회 처리로 수정함 2026-06-01 14:18:28 +09:00
changwng 6432b9f44a [st frontend] sncnSeComCd 인가 구분 공통 처리 2026-06-01 12:37:06 +09:00
changwng 98f24d8a96 [st frontend] 설치인가 검토단계 코드를 추가함 2026-06-01 11:16:02 +09:00
mind-one005 887549c755 wr: 하수재이용시설 현황 수정 2026-05-29 18:09:59 +09:00
changwng d3ca402e4a [st frontend] 첨부 까지 해서 저장함 2026-05-29 15:57:30 +09:00
chanii8 9ca0e96525 Merge branch 'iipbs/as/dev'
Deploy Frontend / deploy (push) Successful in 4m9s
2026-05-29 14:41:52 +09:00
chanii8 4c28db79df Merge branch 'iipbs/as/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/as/dev 2026-05-29 14:40:48 +09:00
chanii8 6e2a957e99 [as] feat: 필수첨부파일관리 페이지 작업 2026-05-29 14:40:41 +09:00
changwng b5eea1a476 [st frontend] as 수정에 체크리스트 데이터 까지 연결함 2026-05-29 14:39:03 +09:00
changwng 5d31c70e16 [st frontend] as 수정에 폼은 등록폼을 같이 쓰도록 코딩함 2026-05-29 14:31:34 +09:00
changwng a6f05b3850 Merge remote-tracking branch 'origin/iipbs/as/dev' into iipbs/as/dev
# Conflicts:
#	apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx
2026-05-29 13:25:46 +09:00
changwng 49ec6423bd [st frontend] as 사용자 뷰 정보 표시 정리 2026-05-29 13:25:16 +09:00
changwng 68ac1168e9 [st frontend] 상세 페이지 수정 작업 2026-05-29 11:19:49 +09:00
mind-one005 e875df1e55 wr: 하수재이용시설 현황 pageSize설정 2026-05-28 17:48:35 +09:00
chanii8 69bd924cd2 [as] refactor: loginService -> authService 2026-05-28 16:56:15 +09:00
chanii8 164bffc5de Merge branch 'iipbs/st/dev' 2026-05-28 16:10:43 +09:00
chanii8 613f2a36d9 Merge branch 'iipbs/cs/dev' 2026-05-28 16:10:20 +09:00
chanii8 0eee6bf338 [st] refactor: authService 내보내기 방식 변경 2026-05-28 16:09:43 +09:00
mind-one005 c2fdc3aa71 fo: /sites/FO 여러번 호출 수정 2026-05-28 15:31:03 +09:00
changwng e98fe69d68 [st frontend] st frontend 분석 2026-05-28 13:59:53 +09:00
changwng 426e3ffcdf [as frontend] 입력화면 콤포넌트화 처리 2026-05-28 13:59:28 +09:00
chanii8 2e25a0311c [cs] refactor: authService 내보내기 방식 변경 2026-05-28 13:41:53 +09:00
chanii8 97df2c47de Merge branch 'iipbs/as/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/as/dev 2026-05-28 13:38:55 +09:00
chanii8 f7c1cf0718 [as] feat: 필수첨부파일관리 페이지 초안 작업 2026-05-28 13:37:36 +09:00
mind-one005 4d0e39721f Merge branch 'iipbs/wr/dev' 2026-05-28 09:13:23 +09:00
changwng a87f887635 [as instl] 기타 파일 삭제시 실제 첨부 파일에 대해서 삭제여부 Y로 변경 처리 로직 추가 2026-05-27 18:42:47 +09:00
changwng 153f3b7fa7 [as instl] user-store.ts 에 로그인 사용자 필드 추가 및 등록페이지에서 사용 처리 2026-05-27 17:28:37 +09:00
jcchoi 668e750b03 Merge remote-tracking branch 'origin/iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 4m23s
2026-05-27 17:20:24 +09:00
rsone001 7e152cacd2 [st] - feat: realgrid 셀 편집 체크 설정 수정, 컴포넌트 url 방식 테스트 중 2026-05-27 17:04:46 +09:00
mind-one005 ac00f978be fo: 빌드오류 수정 2026-05-27 16:52:30 +09:00
mind-one005 c313c1d6cc fo: 지하수 유입량 추정 api 수정 외 2026-05-27 16:15:30 +09:00
chanii8 215d742270 [st] docs: 필수첨부파일관리 페이지 스펙 문서 초안 작성 2026-05-26 17:22:08 +09:00
changwng aa213ceead [as instl] api 에서 as-common-service, as-instl-service, as-checklist-service 의 서비스를 통합관리 as-service로 통합하게 소스 수정함 2026-05-26 16:39:48 +09:00
mind-one005 4eb031ae67 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-05-26 16:05:04 +09:00
mind-one005 b0cd0c772f Merge commit 'e75e38a0b8c00972c158c063ce66664e0b8c2594' 2026-05-26 16:04:47 +09:00
mind-one005 e75e38a0b8 fo: 오수발생량 추정 api 수정 2026-05-26 15:55:50 +09:00
chanii8 86a95ca6e4 Merge branch 'iipbs/st/dev' 2026-05-26 13:19:31 +09:00
chanii8 99fe41b41d [st] feat: H-B 통계검증, I-MR 검증이력 페이지 수정 2026-05-26 13:18:57 +09:00
mind-one005 a78059ec67 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-05-22 16:54:47 +09:00
changwng 50bc502251 [as instl] 기타 파일 그룹 ID 동일하게 업로드 하도록 변경함 2026-05-22 16:02:34 +09:00
mind-one005 7cbde80466 dm: 콘텐츠 관련 refresh 2026-05-22 14:56:58 +09:00
changwng f952f8c262 Merge remote-tracking branch 'origin/iipbs/as/dev' into iipbs/as/dev 2026-05-22 14:36:46 +09:00
changwng 44edac8fbb [as instl] [as 서비스] dongId => admdstCd 로 컬럼 변경 ALTER TABLE sm_bs_as.tb_as_design_cnstn_mng RENAME COLUMN dong_id TO admdst_cd;
ALTER TABLE sm_bs_as.tb_as_fnrs_cnsltn_mng RENAME COLUMN dong_id TO admdst_cd;
                                                ALTER TABLE sm_bs_as.tb_as_instl_sncn_mng RENAME COLUMN dong_id TO admdst_cd;
                                                ALTER TABLE sm_bs_as.tb_as_sewer_bsc_plan RENAME COLUMN dong_id TO admdst_cd; COMMENT ON COLUMN sm_bs_as.tb_as_design_cnstn_mng.admdst_cd IS '행정구역코드';
                                                COMMENT ON COLUMN sm_bs_as.tb_as_fnrs_cnsltn_mng.admdst_cd IS '행정구역코드';
                                                COMMENT ON COLUMN sm_bs_as.tb_as_instl_sncn_mng.admdst_cd IS '행정구역코드';
                                                COMMENT ON COLUMN sm_bs_as.tb_as_sewer_bsc_plan.admdst_cd IS '행정구역코드';
2026-05-22 14:36:41 +09:00
changwng 0e8e646547 [as instl] 설치인가 필수 첨부 파일 수정 2026-05-22 14:11:13 +09:00
chanii8 357dce2161 fix: Cannot find module 에러
Deploy Frontend / deploy (push) Successful in 2m24s
2026-05-22 14:02:48 +09:00
chanii8 430148d843 Merge branch 'iipbs/as/dev' 2026-05-22 13:53:22 +09:00
chanii8 21dab0ef1f [as] feat: 체크리스트관리 및 결재선관리 페이지 수정 2026-05-22 13:52:02 +09:00
changwng da8f66fca1 [as instl] tree를 /list/tree로 url 변경 처리 2026-05-22 13:18:57 +09:00
changwng 31ee840072 [as instl] file upload 시 Middleware 수정 처리 2026-05-22 12:50:23 +09:00
changwng e3d022fcd7 [as instl] 파일 업로드 로직 추가 2026-05-21 18:58:51 +09:00
changwng 09d4750a67 Merge remote-tracking branch 'origin/iipbs/as/dev' into iipbs/as/dev 2026-05-21 18:30:58 +09:00
changwng 8ab63870d4 [as instl] 상태 업데이트 로직 개선: 기존 키의 변화에 따라 상태를 업데이트하도록 수정 Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render. 2026-05-21 18:30:53 +09:00
changwng d3434060ba [as instl service] 신청 저장 로직 추가 2026-05-21 18:25:50 +09:00
chanii8 641132dd56 Merge branch 'iipbs/as/dev'
Deploy Frontend / deploy (push) Successful in 7m56s
2026-05-21 17:39:32 +09:00
chanii8 28bd0cbbf0 Merge branch 'iipbs/as/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/as/dev 2026-05-21 17:39:07 +09:00
chanii8 df4208d4e1 [as] feat: 결재선관리 페이지 작업 2026-05-21 17:38:50 +09:00
changwng ad13875ce8 [as instl service]체크리스트, 공종 리스트 화면 구성 2026-05-21 16:56:12 +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
changwng 9673ee0ed2 [as instl] 업무 구분 선택 화면 추가 2026-05-21 14:49:39 +09:00
changwng d864cf73b9 [as instl] 신청 버튼 클릭시 업무구분 팝업 조회로직으로 해서 추가함 2026-05-21 14:33:25 +09:00
kiminchan b4033071a1 하수관로 편집 상세정보 편집 추가, 하수관망도 업로드 시설 선택 보완 2026-05-21 13:57:16 +09:00
changwng 9dec42a584 [as instl] 설치인가 등록페이지 설정 2026-05-21 13:42:50 +09:00
mind-one005 9b4d2d410d Merge branch 'iipbs/dm/dev' 2026-05-21 13:17:39 +09:00
changwng 9e7dea7e9f Merge remote-tracking branch 'origin/iipbs/as/dev' into iipbs/as/dev 2026-05-21 13:13:36 +09:00
changwng 6938c1e96f [as instl service] 회계 코드 연결 작업 2026-05-21 13:13:30 +09:00
mind-one005 4783bbacbd dm: login 입력란 초기화 2026-05-21 11:43:24 +09:00
chanii8 0ef375310f [as] refactor: useConfirm 적용 2026-05-21 11:35:47 +09:00
mind-one005 3eb028b075 Merge branch 'iipbs/wr/dev' 2026-05-20 18:00:42 +09:00
mind-one005 c1a03fcabc wr: 대시보드 표로보기 2026-05-20 17:50:43 +09:00
chanii8 24c446b880 [as] feat: 체크리스트 관리 페이지 생성 2026-05-20 17:33:05 +09:00
mind-one005 48172aa3f0 dm: 사례 콘텐츠 2026-05-20 17:16:56 +09:00
mind-one005 adf5e288d3 Merge branch 'iipbs/dm/dev' 2026-05-20 14:12:09 +09:00
mind-one005 d8d5755849 dm: 소개 html 기록 2026-05-20 14:10:44 +09:00
mind-one005 9f803fd4ad dm: build 에러수정 2026-05-20 09:14:14 +09:00
chanii8 cc4b2ca084 Merge branch 'iipbs/as/dev'
Deploy Frontend / deploy (push) Successful in 2m16s
2026-05-19 17:39:49 +09:00
chanii8 ab3c4e77ee [as] feat: 결재선 추가 로직 수정 2026-05-19 17:39:00 +09:00
chanii8 3c0a16ea67 [as] docs: 체크리스트관리 스펙 문서 초안 생성 2026-05-19 16:31:43 +09:00
chanii8 a6da87837a [as] feat: 체크리스트 엔티티 생성 2026-05-19 16:17:09 +09:00
chanii8 21533c8c0e Merge branch 'iipbs/as/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/as/dev 2026-05-19 16:01:43 +09:00
chanii8 3f75d2751a [as] rename: steiger 적용 2026-05-19 16:01:38 +09:00
chanii8 c5a5b3bde3 [st] feat: 결재선 순서 검증 로직 추가 2026-05-19 14:59:22 +09:00
kiminchan 0c66a8f3b7 통수위험분석 프로토타입 구현 2026-05-19 13:39:25 +09:00
mind-one005 75e619a997 dm: 컨텐츠2 2026-05-19 11:37:07 +09:00
changwng 02f5fae903 Merge branch 'iipbs/as/dev' 2026-05-19 11:03:50 +09:00
changwng 1e3a2fea8d Merge remote-tracking branch 'origin/iipbs/as/dev' into iipbs/as/dev 2026-05-19 11:03:19 +09:00
changwng a4e73284ed [as instl service] vscode 디버그 모듈 활성화 처리및 설치인가 페이지 추가 2026-05-19 11:03:14 +09:00
chanii8 c94d9019d3 [as] refactor: 결재선관리 PK 정책 변경으로 인한 수정 2026-05-19 10:55:23 +09:00
mind-one005 a9d78a7e0d dm: 컨텐츠1 2026-05-19 09:52:18 +09:00
changwng d93157b10a Merge remote-tracking branch 'origin/main' 2026-05-19 09:48:36 +09:00
jcchoi ad18cf1c16 Merge remote-tracking branch 'origin/iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 3m4s
2026-05-18 19:38:04 +09:00
rsone001 90834f3493 [st] - feat: 미입력현황 조회 2026-05-18 19:36:13 +09:00
chanii8 535a6f1ef2 Merge branch 'iipbs/as/dev'
Deploy Frontend / deploy (push) Successful in 2m40s
2026-05-18 19:15:39 +09:00
chanii8 86566c7692 [as] feat: 결재선관리 페이지 작업 2026-05-18 19:08:38 +09:00
chanii8 51204e2ec1 Merge branch 'iipbs/as/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/as/dev 2026-05-18 17:40:49 +09:00
chanii8 25affc06b9 [as] feat: 결재선 관리 페이지 구현 2026-05-18 17:40:21 +09:00
jcchoi ad5fc8a2de Merge remote-tracking branch 'origin/iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 3m5s
2026-05-18 17:24:30 +09:00
changwng e2cf4f3f34 Merge remote-tracking branch 'origin/main' 2026-05-18 17:23:08 +09:00
jcchoi 8272093e07 컬럼 수정 2026-05-18 17:22:19 +09:00
rsone001 ba42b74578 Merge remote-tracking branch 'origin/iipbs/st/dev' 2026-05-18 17:21:21 +09:00
jcchoi fba84202f0 검증 수정
Deploy Frontend / deploy (push) Successful in 2m54s
2026-05-18 17:07:09 +09:00
jcchoi f70a20c0c7 검증 수정 2026-05-18 17:04:16 +09:00
rsone001 ac75188980 [st] - fix: DataGrid height 변경 2026-05-18 17:02:12 +09:00
rsone001 404624a583 [st] - fix: 통계이력조회 메뉴 구조 변경 2026-05-18 16:57:18 +09:00
mind-one005 696fdf84d2 wr: 구분에 따른 조회 2026-05-18 16:39:40 +09:00
rsone001 91e505f37f [st] - fix: 권한 및 데이터 상태에 따른 버튼 표출 조건식 오류 수정, 2-6 & 6-2 저장 가능 여부 기능 추가 2026-05-18 16:26:10 +09:00
mind-one005 294fea4f71 Merge remote-tracking branch 'origin/iipbs/dm/dev' into iipbs/wr/dev 2026-05-18 16:18:40 +09:00
jcchoi 148275626c Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev
Deploy Frontend / deploy (push) Successful in 2m58s
2026-05-18 16:16:12 +09:00
jcchoi 7a0a8f23b7 컬럼추가 2026-05-18 16:15:08 +09:00
jcchoi 45bf7b10f3 Merge branch 'iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 2m58s
2026-05-18 16:11:22 +09:00
jcchoi 246f6472a3 검증리포트 항목추가 2026-05-18 16:08:56 +09:00
mind-one005 965b8f3feb wr: 하수재이용시설 현황 조회 수정 2026-05-18 14:47:12 +09:00
jcchoi c37696dee3 Merge branch 'iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 3m0s
2026-05-18 14:44:10 +09:00
LGS fb61080d8d 물 재이용 소개 - 타법령관의관계검토 이미지 교체 2026-05-18 14:36:26 +09:00
jcchoi d4f46ca344 Merge branch 'origin/iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 2m6s
2026-05-18 14:28:02 +09:00
rsone001 50ed22b4bc [st] - fix: 메뉴 권한체크 로직 수정 2026-05-18 12:06:56 +09:00
changwng b0dbd78410 Merge remote-tracking branch 'origin/iipbs/as/dev' 2026-05-18 11:38:15 +09:00
rsone001 b508568e86 [st] - fix: 오류 수정 및 누락 기능 추가 2026-05-18 11:03:43 +09:00
rsone001 8384743a30 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-05-18 11:01:20 +09:00
kiminchan ef7bae98d4 하수관망도 업로드 구현 2026-05-18 10:23:33 +09:00
changwng ea9bd64088 Merge remote-tracking branch 'origin/main' 2026-05-18 09:58:23 +09:00
changwng 758d5e7fac [as service] 디버그 모드 활성화및 INSTL_SNCN 설치인가 디렉토리 구성 2026-05-18 09:27:42 +09:00
changwng 658a20f587 feat: package.json 파일에 dev st, as 필터 추가함 2026-05-18 09:26:12 +09:00
mind-one005 9661369708 wr: layout 수정 2026-05-15 18:05:57 +09:00
yangsein 4e8f492006 feat:01.하수도보급률 농어촌 보급률 컬럼 추가
02.하수관로 관종별현황 노후하수관로율 컬럼추가
2026-05-15 16:01:20 +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-one005 12e9c2a30c wr: 재이용시설등록 10건 , Uitable 수정 2026-05-15 14:59:33 +09:00
jcchoi 6539e5bb80 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into origin/iipbs/st/dev 2026-05-15 13:04:02 +09:00
jcchoi 91116ccdf7 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend
Deploy Frontend / deploy (push) Successful in 4m12s
2026-05-15 11:41:30 +09:00
jcchoi 2bcc49e090 검증결과 리포트 수정정 2026-05-15 11:41:23 +09:00
jcchoi c013759869 그리드 색상 수수정(머지시 , 롤오버시) 2026-05-15 11:41:05 +09:00
mind-one005 bdbba42616 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-05-14 17:50:11 +09:00
mind-one005 a84b233435 Merge branch 'iipbs/dm/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/dm/dev 2026-05-14 17:04:27 +09:00
mind-one005 4340a57180 dm: 컨텐츠 조회 2026-05-14 17:04:19 +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
LGS 1cc6b4b565 메인 컨텐츠 layout 內 세로스크롤 overflow-x-auto 제거 및 헤더 중복 제거 2026-05-13 15:39:27 +09:00
mind-one005 4a6cfed9bb dm: 스크롤 이상 수정 2026-05-13 15:24:56 +09:00
chanii8 0e067bb5fc [as] chore: 로그인 페이지 문구 수정 2026-05-12 16:04:51 +09:00
1344 changed files with 127161 additions and 19198 deletions
+1
View File
@@ -29,4 +29,5 @@ settings.json
.agent
apps/**/tsconfig.tsbuildinfo
packages/**/tsconfig.tsbuildinfo
logs/
+88
View File
@@ -0,0 +1,88 @@
{
// IntelliSense를 사용하여 가능한 특성에 대해 알아보세요.
// 기존 특성에 대한 설명을 보려면 가리킵니다.
// 자세한 내용을 보려면 https://go.microsoft.com/fwlink/?linkid=830387을(를) 방문하세요.
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "dev:as 디버그",
"runtimeExecutable": "pnpm",
"runtimeArgs": [
"run",
"dev:as"
],
"skipFiles": [
"<node_internals>/**"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"sourceMaps": true,
"env": {
"NODE_OPTIONS": "--inspect"
}
},
{
"type": "node",
"request": "launch",
"name": "dev:cs 디버그",
"runtimeExecutable": "pnpm",
"runtimeArgs": [
"run",
"dev:cs"
],
"skipFiles": [
"<node_internals>/**"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"sourceMaps": true,
"env": {
"NODE_OPTIONS": "--inspect"
}
},
{
"type": "node",
"request": "attach",
"name": "dev:cs 서버 연결 (attach)",
"port": 9229,
"restart": true,
"skipFiles": [
"<node_internals>/**"
],
"sourceMaps": true,
"resolveSourceMapLocations": [
"${workspaceFolder}/apps/cs/**",
"!**/node_modules/**"
]
},
{
"type": "chrome",
"request": "launch",
"name": "dev:cs 클라이언트 디버그 (Chrome)",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}/apps/cs",
"sourceMaps": true
},
{
"type": "node",
"request": "launch",
"name": "dev:st 디버그",
"runtimeExecutable": "pnpm",
"runtimeArgs": [
"run",
"dev:st"
],
"skipFiles": [
"<node_internals>/**"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"sourceMaps": true,
"env": {
"NODE_OPTIONS": "--inspect"
}
}
]
}
+5
View File
@@ -0,0 +1,5 @@
{
"chat.tools.terminal.autoApprove": {
"pnpm": true
}
}
+2 -2
View File
@@ -19,7 +19,7 @@
"@tanstack/react-table": "^8.21.2",
"axios": "^1.8.1",
"dayjs": "^1.11.13",
"next": "15.3.8",
"next": "15.5.18",
"react-hook-form": "^7.54.2",
"zod": "^3.24.2"
},
@@ -33,4 +33,4 @@
"tailwindcss": "^3.4.14",
"typescript": "^5.7.2"
}
}
}
+9 -5
View File
@@ -19,11 +19,15 @@
}
],
"paths": {
"@/*": ["./src/*"],
"@common/components": ["../../packages/common/components/dist/"],
"@common/network": ["../../packages/common/network/"]
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"../../packages/common/types/**/*.d.ts"
],
"exclude": ["node_modules", ".next", "dist", "coverage", "storybook-static", "public"]
}
@@ -0,0 +1 @@
export { ApprovalLineManagementPage as default } from "@/pages/approval-line-management";
@@ -0,0 +1 @@
export { ChecklistManagementPage as default } from "@/pages/checklist-management";
@@ -0,0 +1 @@
export { CodeManagementPage as default } from "@/pages/code-management";
@@ -0,0 +1,10 @@
import { DesignEditPage } from "@/pages/design-cnstn";
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function Page({ params }: PageProps) {
const { id } = await params;
return <DesignEditPage id={id} />;
}
@@ -0,0 +1,10 @@
import { DesignViewPage } from "@/pages/design-cnstn";
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function Page({ params }: PageProps) {
const { id } = await params;
return <DesignViewPage id={id} />;
}
@@ -0,0 +1,3 @@
import { DesignListPage } from "@/pages/design-cnstn";
export default DesignListPage;
@@ -0,0 +1,12 @@
import { DesignRegistPage } from "@/pages/design-cnstn";
import { parseTaskSeComCdFromApi } from "@/pages/design-cnstn/lib/design-cnstn-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 <DesignRegistPage taskSeComCd={taskSeComCd.length ? taskSeComCd : undefined} />;
}
@@ -0,0 +1 @@
export { EsntlAtchFileMngPage as default } from "@/pages/esntl-atch-file-mng";
@@ -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} />;
}
@@ -0,0 +1,6 @@
import { InstlViewPage } from "@/pages/instl-sncn";
export default async function InstlSncnDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
return <InstlViewPage id={id} />;
}
@@ -0,0 +1,3 @@
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} />;
}
@@ -0,0 +1 @@
export { RvwMnpwMngPage as default } from "@/pages/rvw-mnpw-mng";
+9 -9
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");
@@ -38,7 +43,8 @@ async function handler(
body = await request.json();
}
const result = await fetch(`${SERVER_API_URL}/${params.url.join("/")}`, {
const targetUrl = `${SERVER_API_URL}/${params.url.join("/")}`;
const result = await fetch(targetUrl, {
method,
headers,
body: JSON.stringify(body),
@@ -58,18 +64,11 @@ async function handler(
[AUTH_USER_ID]: userId,
});
const forwardedHost = request.headers.get("x-forwarded-host");
const host = request.headers.get("host");
const actualHost = forwardedHost || host || new URL(request.url).hostname.toString();
// 운영 환경에서 domain 설정
const domain = actualHost.includes("hasuon.or.kr") ? ".hasuon.or.kr" : actualHost;
if (refreshToken) {
cookieStore.set(REFRESH_TOKEN, refreshToken, {
httpOnly: true,
sameSite: "strict",
domain: domain,
domain: COOKIE_DOMAIN,
path: "/",
});
}
@@ -79,6 +78,7 @@ async function handler(
return NextResponse.json({ message: "Invalid Credentials" }, { status: 401 });
} catch (error) {
console.error(`[Proxy API] Error caught in handler:`, error);
return NextResponse.json({ message: "Server Error", error }, { status: 500 });
}
}
+280 -231
View File
@@ -4,7 +4,7 @@
| 항목 | 내용 |
| ---------- | -------------------------------------------- |
| 버전 | 0.1 (초안) |
| 버전 | 0.3 |
| 프로세스명 | UI설계 |
| 화면명 | 결재선 관리 |
| 화면 유형 | [v] 조회 [v] 입력 [v] 수정 [v] 삭제 [ ] 출력 |
@@ -14,7 +14,7 @@
## 2. 화면 목적
전자결재 처리에 사용할 **결재서식**(템플릿)을 등록·관리하고, 각 서식별로 **결재선**(접수 -> 검토 -> 승인 -> 최종승인 단계)을 구성한다. 상단의 결재서식 목록에서 하나를 선택하면 하단 영역에 해당 서식의 결재 단계(결재자/순서/구분)가 표시되며, 사용자는 결재자 추가·수정·삭제와 결재선 저장을 수행한다.
전자결재 처리에 사용할 **결재서식**(템플릿)을 등록·관리하고, 각 서식별로 **결재선**(결재구분·결재자·순서)을 구성한다. 상단의 결재서식 목록에서 하나를 선택하면 하단 영역에 해당 서식의 결재선이 표시되며, 사용자는 **그리드 인라인 폼**으로 결재자를 등록·수정한다. **행 추가 또는 한 행 수정 중에는 다른 행을 편집할 수 없다**(단일 행 편집 잠금). 드래그로 순서를 변경한 뒤 **[결재선 저장]** 으로 서버에 반영한다.
---
@@ -23,30 +23,27 @@
### 3-1. 전체 레이아웃
```
┌──────────────────────────────────────────────────────────────────────────────┐
│ 페이지 타이틀: 결재선 관리 브레드크럼: 디지털행정지원 > … > │
├──────────────────────────────────────────────────────────────────────────────┤
│ [영역 A] 전자결재선 관리 목록 [서식 등록] │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ 전자결재서식아이디 | 결재서식명 | 문서구분 | 등록일시 | 관리 │ │
│ ├──────────────────────────────────────────────────────────────────────┤ │
│ │ TMPL-0001 (선택 행 강조) … [수정][삭제] │ │
│ │ TMPL-0002 … [수정][삭제] │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────────────────────┤
│ [영역 B] 결재선 관리 선택된 서식: TMPL-0001 │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ [영역 B-1] 결재자 입력 폼 │ │
│ │ 결재선아이디(자동발급) | 결재구분 ▾ | 결재자명 [검토인력 선택] │ │
│ │ 결재순서번호 [결재자 추가] [결재선 저장] [초기화] │ │
│ ├──────────────────────────────────────────────────────────────────────┤ │
│ │ [영역 B-2] 결재선 그리드 │ │
│ │ 결재선아이디 | 결재순서번호 | 결재구분 | 결재자명 | 등록일시 | 관리 │ │
│ │ APLN-0001 | 1 | 접수 | 김민수 | … |[수정][삭제] │
│ │ APLN-0002 | 2 | 검토 | 박지훈 | … |[수정][삭제] │
│ │ APLN-0003 | 3 | 최종승인 | 이서연 | … |[수정][삭제] │
│ └──────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────────────────────────┐
│ 페이지 타이틀: 결재선 관리 브레드크럼: 디지털행정지원 > … > │
├───────────────────────────────────────────────────────────────────────────────────────────────┤
│ [영역 A] 전자결재선 관리 목록 [서식 등록] │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ 전자결재서식아이디 | 결재서식명 | 문서구분 | 등록일시 | 관리 │ │
│ ├──────────────────────────────────────────────────────────────────────┤ │
│ │ TMPL-0001 (선택 행 강조) … [수정][삭제] │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
├───────────────────────────────────────────────────────────────────────────────────────────────┤
│ [영역 B] 결재선 관리 선택된 서식: TMPL-0001 [결재선 저장] │
│ ┌─────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ [영역 B-2] 결재선 그리드 (인라인 편집 + 행 드래그 — @dnd-kit) │ │
│ │ ⋮⋮ | 결재선아이디 | 결재순서 | 결재구분 ▼ | 결재자 [선택] | 등록일시 | 관리 │ │
│ │ ⋮⋮ | APLN-0001 | [ 1 ] | [검토자 ▼] | [김민수 ][선택] | … | [저장][삭제] │ │
│ │ ⋮⋮ | (신규) | [ 2 ] | [승인자 ▼] | [ ][선택] | — | [저장][취소] │ │
│ └─────────────────────────────────────────────────────────────────────────────────────────┘ │
│ [행 추가] │
│ ※ 결재자 등록·수정은 그리드 셀에서 직접 입력. 검토인력만 [선택] 팝업/다이얼로그 │
│ ※ 행 추가·수정 중: 해당 행만 편집 가능, 나머지 행·[행 추가]·드래그 비활성 │
└───────────────────────────────────────────────────────────────────────────────────────────────┘
```
### 3-2. 화면 데이터 흐름
@@ -55,102 +52,116 @@
BEGIN
// 최초 진입
CALL TBD.결재서식목록조회()
CALL GET /as-elapr-service/api/v1/elapr-tmplt
DISPLAY 결재서식 그리드 (영역 A)
DISPLAY 빈 상태 안내 (영역 B) // 서식 미선택 시 폼·그리드 비활성
DISPLAY 빈 상태 안내 (영역 B) // 서식 미선택 시 그리드·툴바 비활성
// 영역 A: 서식 행 선택
ON 영역 A 행 CLICK DO
SET 선택된 서식 = 행.전자결재서식아이디 // 영역 B 상단 라벨에 표시
CALL TBD.결재선목록조회(서식아이디)
DISPLAY 결재선 그리드 (영역 B-2)
RESET 결재자 입력 폼 (영역 B-1)
SET 선택된 서식 = 행.elaprTmpltId
CALL GET /as-elapr-service/api/v1/elapr-apln?elaprTmpltId={id}
DISPLAY 결재선 그리드 (영역 B-2) — 조회 모드
END ON
// 영역 A: 신규 서식 등록
ON [서식 등록] 버튼 CLICK DO
OPEN 결재서식 등록 다이얼로그
ON 확인 DO
CALL TBD.결재서식등록(서식정보)
CALL TBD.결재서식목록조회() // → 영역 A 갱신
CALL POST /as-elapr-service/api/v1/elapr-tmplt
CALL GET /as-elapr-service/api/v1/elapr-tmplt
END ON
END ON
// 영역 A: 서식 수정·삭제
ON 영역 A [수정] 버튼 CLICK DO
OPEN 결재서식 수정 다이얼로그(선택 행)
ON 확인 DO
CALL TBD.결재서식수정(서식아이디, 서식정보)
CALL TBD.결재서식목록조회()
END ON
// 영역 A: 서식 수정·삭제 (다이얼로그 유지)
ON 영역 A [수정] / [삭제] DO
(기존과 동일 — PUT / DELETE elapr-tmplt)
END ON
ON 영역 A [삭제] 버튼 CLICK DO
CONFIRM "선택한 결재서식을 삭제하시겠습니까?"
CALL TBD.결재서식삭제(서식아이디)
CALL TBD.결재서식목록조회()
IF 삭제된 서식이 현재 선택된 서식 THEN
RESET 영역 B (그리드 비우기, 폼 비활성)
END IF
END ON
// 영역 B-1: 결재자 입력 폼
ON [검토인력 선택] 버튼 CLICK DO
VALIDATE 서식 선택됨 // 미선택 시 안내 후 종료
OPEN 검토인력 선택 다이얼로그
ON 선택 DO
SET 결재자명 = 선택인력.성명
SET 결재자ID = 선택인력.사용자ID // 내부 상태로 보관
END ON
END ON
ON 결재구분 SELECT CHANGE DO
SET 결재구분 = 선택값 // 접수 / 검토 / 최종승인
END ON
ON 결재순서번호 INPUT CHANGE DO
SET 결재순서번호 = 값(숫자)
END ON
// 영역 B-1: 결재자 추가 (그리드에 행 추가, 미저장 상태)
ON [결재자 추가] 버튼 CLICK DO
// 영역 B: 행 추가 (인라인 신규 행)
ON [행 추가] 버튼 CLICK DO
VALIDATE 서식 선택됨
· 결재구분 입력
· 결재자 선택됨
· 결재순서번호 > 0 (중복 시 안내)
ADD 입력 폼 값을 영역 B-2 그리드에 임시 행으로 추가
RESET 결재자 입력 폼 (영역 B-1)
// 결재선아이디는 저장 시점에 서버에서 자동 발급되므로
// 임시 행은 클라이언트 임시 키로 식별 (예: APLN-0007 같은 미발급 표기 또는 임시 ID)
VALIDATE 편집 중인 행 없음 (editingRowKey == null)
ADD 영역 B-2 그리드 맨 아래 편집 가능 행
SET editingRowKey = rowKey (draft-{timestamp})
SET 편집상태 = 신규
SET aplnId = 미설정 (POST 저장 시 서버 fn_gen_apln_id 발급)
SET atrzSeqNo = 현재 행 수 + 1
SET atrzSeComCd, aprvrUuid = 빈 값
DISABLE 나머지 행 편집 · [행 추가] · 드래그 · [결재선 저장](정책에 따라)
END ON
// 영역 B-1: 결재선 저장 (그리드 전체 일괄 저장)
// 영역 B-2: 기존 행 수정 진입
ON 행별 [수정] CLICK DO
VALIDATE 편집 중인 행 없음
SET editingRowId = 행.aplnId
SET 해당 행만 편집 모드
DISABLE 나머지 행 편집 · [행 추가] · 드래그
END ON
// 영역 B-2: 인라인 편집
ON 그리드 셀 변경 DO
VALIDATE 변경 대상 행 == editingRowId
SET 해당 행 dirty = true
END ON
ON 셀 [선택] (검토인력) CLICK DO
OPEN 검토인력 선택 다이얼로그 (단일 선택)
ON 선택 DO
SET 행.aprvrUuid = 선택인력.uuid
SET 행.aprvrNm = 선택인력.성명 // UI 표시용 (응답 DTO에 없음 — 로컬 보조 필드)
END ON
END ON
// 영역 B-2: 행별 저장
ON 행별 [저장] CLICK DO
VALIDATE atrzSeComCd, aprvrUuid, atrzSeqNo
IF 편집상태 = 신규 THEN
CALL POST /as-elapr-service/api/v1/elapr-apln
ELSE
CALL PUT /as-elapr-service/api/v1/elapr-apln/{aplnId}
END IF
CALL GET /as-elapr-service/api/v1/elapr-apln?elaprTmpltId={id}
CLEAR editingRowId
ENABLE 전체 행 조회 모드 · [행 추가] · 드래그
END ON
// 영역 B: 결재선 저장 (드래그 순서 일괄 반영)
ON [결재선 저장] 버튼 CLICK DO
VALIDATE 서식 선택됨 · 결재선 1건 이상 · 순서번호 중복 없음
CALL TBD.결재선일괄저장(서식아이디, 결재선목록)
CALL TBD.결재선목록조회(서식아이디) // → 영역 B-2 갱신
VALIDATE 서식 선택됨
VALIDATE 편집 중인 행 없음
REORDER atrzSeqNo = 그리드 행 순서 (1..n)
FOR EACH dirty 또는 순서 변경된 행 DO
CALL PUT /as-elapr-service/api/v1/elapr-apln/{aplnId} // atrzSeqNo 등 반영
END FOR
FOR EACH 신규 미저장 행 DO
CALL POST /as-elapr-service/api/v1/elapr-apln
END FOR
CALL GET /as-elapr-service/api/v1/elapr-apln?elaprTmpltId={id}
TOAST "결재선이 저장되었습니다."
END ON
// 영역 B-1: 초기화
ON [초기화] 버튼 CLICK DO
RESET 결재자 입력 폼 (영역 B-1) → 기본값
(결재구분=접수, 결재자명="", 결재순서번호=1)
// 영역 B-2: 행 드래그
ON 영역 B-2 행 DRAG END DO
VALIDATE 편집 중인 행 없음
REORDER 그리드 행
SET 각 행 atrzSeqNo = 행 인덱스 + 1 (UI 즉시 반영, 서버는 [결재선 저장] 또는 행별 [저장])
END ON
// 영역 B-2: 그리드 행 수정·삭제
ON 영역 B-2 [수정] 버튼 CLICK DO
OPEN 결재자 수정 다이얼로그(선택 행)
ON 확인 DO
CALL TBD.결재선수정(결재선아이디, 결재선정보)
CALL TBD.결재선목록조회(서식아이디)
END ON
// 영역 B-2: 행 삭제
ON 행별 [삭제] CLICK DO
CONFIRM "선택한 결재선을 삭제하시겠습니까?"
IF 서버 저장된 행 THEN
CALL DELETE /as-elapr-service/api/v1/elapr-apln/{aplnId}
END IF
REMOVE 그리드 행 (신규 미저장 행은 API 호출 없이 제거)
CALL GET /as-elapr-service/api/v1/elapr-apln?elaprTmpltId={id}
END ON
ON 영역 B-2 [삭제] 버튼 CLICK DO
CONFIRM "선택한 결재자를 삭제하시겠습니까?"
CALL TBD.결재선삭제(결재선아이디)
CALL TBD.결재선목록조회(서식아이디)
ON 행별 [취소] CLICK DO // 신규·수정 공통
IF 편집상태 = 신규 THEN REMOVE 임시 행
ELSE RESTORE 행 스냅샷 (수정 취소)
CLEAR editingRowId
ENABLE 전체 행 조회 모드 · [행 추가] · 드래그
END ON
END
@@ -171,135 +182,179 @@ END
### 4-2. 그리드 컬럼
| 컬럼명 | Field | 타입 | 비고 |
| ------------------ | ----------------- | ------ | --------------------------------------------- |
| 전자결재서식아이디 | `elctAtrzTmpltId` | string | 예) `TMPL-0001` (서버 자동 발급, TBD) |
| 결재서식명 | `atrzTmpltNm` | string | 예) `설치인가 검토 결재` |
| 문서구분 | `docSeNm` | string | 예) `설치인가`, `기본계획` (코드/명 매핑 TBD) |
| 등록일시 | `regDt` | string | `YYYY-MM-DD HH:mm:ss` |
| 관리 | — | — | 행별 **[수정] · [삭제]** 버튼 |
| 컬럼명 | Field | 타입 | 비고 |
| ------------------ | -------------- | ------ | --------------------------------------- |
| 전자결재서식아이디 | `elaprTmpltId` | string | 등록 시 서버(DB 함수) 자동 발급 |
| 결재서식명 | `elaprTmpltNm` | string | |
| 문서구분 | `docSeComNm` | string | 코드 `docSeComCd`, 상위코드 `ASDOCSECD` |
| 등록일시 | `regDt` | string | `yyyy-MM-dd HH:mm:ss` |
| 관리 | — | — | 행별 **[수정] · [삭제]** 버튼 |
- 행 클릭 시: 해당 서식을 **선택 상태**로 만들고 영역 B 라벨(`선택된 서식: {전자결재서식아이디}`)과 영역 B-2 그리드를 갱신한다.
- 페이지네이션: TBD (스크린샷에는 미표시).
- 행 클릭 시: `elaprTmpltId` 선택 → 영역 B 라벨·B-2 그리드 갱신.
- 페이지네이션: Spring `Pageable` (`page`, `size`, `sort`).
### 4-3. 버튼
| 버튼명 | 위치 | 동작 |
| --------- | -------------- | ----------------------------------------------------------------- |
| 서식 등록 | 섹션 우측 상단 | 결재서식 등록 다이얼로그 오픈 (입력 항목·필수값 TBD) |
| 수정 | 행별 | 결재서식 수정 다이얼로그 오픈 (해당 행 값으로 초기화) |
| 삭제 | 행별 | 확인 다이얼로그 후 삭제. 선택 중인 서식이면 영역 B 상태도 초기화. |
| 버튼명 | 위치 | 동작 |
| --------- | -------------- | ----------------------------------------------- |
| 서식 등록 | 섹션 우측 상단 | 결재서식 등록 다이얼로그 오픈 |
| 수정 | 행별 | 결재서식 수정 다이얼로그 (기존 값 초기화) |
| 삭제 | 행별 | 확인 후 삭제. 선택 중인 서식이면 영역 B 초기화. |
### 4-4. 다이얼로그(개요)
| 다이얼로그명 | 트리거 | 입력 항목(예상) | 비고 |
| ------------------ | ---------------- | ------------------------------ | ------------------------ |
| 결재서식 등록 | [서식 등록] 클릭 | 결재서식명, 문서구분(드롭다운) | 등록 성공 시 영역 A 갱신 |
| 결재서식 수정 | 행별 [수정] | 결재서식명, 문서구분 | 기존 값으로 초기화 |
| 결재서식 삭제 확인 | 행별 [삭제] | (확인/취소) | |
| 다이얼로그명 | 트리거 | 입력 항목 | 비고 |
| ------------------ | ----------- | ----------------------------------------------- | ----------------------------------------- |
| 결재서식 등록 | [서식 등록] | `elaprTmpltNm`, `docSeComCd` | `elaprTmpltExpln` 선택, ID는 저장 후 응답 |
| 결재서식 수정 | 행별 [수정] | `elaprTmpltNm`, `docSeComCd`, `elaprTmpltExpln` | ID는 변경 불가 |
| 결재서식 삭제 확인 | 행별 [삭제] | (확인/취소) | |
---
## 5. 영역 B: 결재선 관리
영역 A에서 선택된 결재서식을 기준으로 결재 단계(접수·검토·최종승인)와 결재자, 순서를 관리한다. 영역 B는 상단 **결재자 입력 폼(B-1)** 과 하단 **결재선 그리드(B-2)** 로 구성된다.
영역 A에서 선택된 `elaprTmpltId` 기준으로 결재선을 관리한다. **등록·수정은 B-2 그리드 인라인 폼**으로 수행하며, 검토인력 지정만 **[선택]** 보조 다이얼로그를 사용한다.
### 5-1. 헤더 / 상태 표시
### 5-1. 헤더 / 툴바
| 항목 | 내용 |
| ----------- | ------------------------------------------------------------------------------------------------------------ |
| 섹션 타이틀 | 결재선 관리 |
| 우측 라벨 | `선택된 서식: {전자결재서식아이디}` — 영역 A에서 선택한 서식 ID를 강조 색으로 표시. 미선택 시 영역 B 비활성. |
| 항목 | 내용 |
| ----------- | --------------------------------------------------------------------------------------------------------------------------- |
| 섹션 타이틀 | 결재선 관리 |
| 우측 라벨 | `선택된 서식: {elaprTmpltId}` — 미선택 시 영역 B 비활성 |
| 우측 버튼 | **[행 추가]** — 편집 중인 행이 없을 때만 활성 · **[결재선 저장]** — 편집 중인 행이 없을 때만 활성(순서·미저장 행 일괄 반영) |
### 5-2. 영역 B-1: 결재자 입력 폼
#### 필드 정의
| 표시명 | 입력 유형 | 필수 | 기본값 | 데이터 소스 | API 파라미터 매핑 | 비고 |
| ---------------------- | ----------------- | ---- | ----------- | -------------------- | ----------------- | --------------------------------------------------------------------- |
| 결재선아이디(자동발급) | 텍스트(읽기 전용) | — | `APLN-XXXX` | 서버 자동 발급 (TBD) | TBD | 입력 불가. 저장 시 서버 발급. 표시값은 미발급 자리표시(또는 임시 ID). |
| 결재구분 | 드롭다운 | 예 | `접수` | 코드 (TBD) | TBD | 옵션: **접수 / 검토 / 최종승인** (그리드 행 기준) |
| 결재자명 | 텍스트(읽기 전용) | 예 | `""` | [검토인력 선택] 결과 | TBD | 자유 입력 불가. 다이얼로그 선택 결과로 채움. |
| 결재순서번호 | 숫자 입력 | 예 | `1` | — | TBD | 1 이상의 정수. 동일 서식 내 중복 검증 필요. |
플레이스홀더 / 표시 텍스트:
- 결재자명 입력란 플레이스홀더: `검토인력을 선택하세요`
- 결재선아이디 표시 예시: `APLN-0007` (다음 발급 예정 번호 노출 또는 빈 칸 — 표시 정책 TBD)
#### 버튼
| 버튼명 | 동작 |
| ------------- | ------------------------------------------------------------------------------------------------------------- |
| 검토인력 선택 | **검토인력 선택 다이얼로그** 오픈. 선택 시 `결재자명`과 내부 `결재자 ID`를 폼 상태에 반영. |
| 결재자 추가 | 입력 폼 값을 영역 B-2 그리드에 **임시 행**으로 추가하고 폼을 초기화한다. (서버 호출 없음, 저장 시 일괄 반영.) |
| 결재선 저장 | 영역 B-2의 결재선 목록을 일괄 저장 API로 전송한다. 성공 시 그리드 재조회 + 토스트 표시. |
| 초기화 | 입력 폼을 기본값으로 리셋(결재구분=접수, 결재자명=빈값, 결재순서번호=1). |
> 정책 메모: **[결재자 추가]** 와 **[결재선 저장]** 의 분리에 대해서는 두 가지 운영이 가능하다. 본 SPEC 초안은 그리드에 임시 행을 누적해 두고 [결재선 저장] 시점에 서버에 일괄 반영하는 모델을 가정한다. 실제 구현 방식(추가 즉시 저장 vs 일괄 저장)은 백엔드 API 확정 후 재정의(TBD).
#### 다이얼로그
| 다이얼로그명 | 트리거 | 표시 내용 / 입력 항목 | 결과 |
| ---------------- | --------------- | ---------------------------------------------------------- | ----------------------------------- |
| 검토인력 선택 | [검토인력 선택] | 사용자 검색 필터(이름/소속 등 TBD), 사용자 목록(단일 선택) | 선택된 사용자 정보를 입력 폼에 반영 |
| 결재자 수정 | B-2 행별 [수정] | 결재구분, 결재자명(선택), 결재순서번호 | 확인 시 해당 행 갱신 |
| 결재자 삭제 확인 | B-2 행별 [삭제] | (확인/취소) | 확인 시 해당 행 삭제 |
### 5-3. 영역 B-2: 결재선 그리드
### 5-2. 영역 B-2: 결재선 그리드 (인라인 편집)
#### 그리드 컬럼
| 컬럼명 | Field | 타입 | 비고 |
| ------------ | ------------ | ------ | ------------------------------------- |
| 결재선아이디 | `atrzLineId` | string | 예) `APLN-0001` (서버 자동 발급, TBD) |
| 결재순서번호 | `atrzSeqNo` | number | 1부터 시작. 단계 순서. |
| 결재구분 | `atrzSeNm` | string | `접수` / `검토` / `최종승인` |
| 결재자명 | `atrzUserNm` | string | 검토인력 선택 시 채워짐 |
| 등록일시 | `regDt` | string | `YYYY-MM-DD HH:mm:ss` |
| 관리 | — | — | 행별 **[수정] · [삭제]** 버튼 |
| 컬럼명 | Field | 입력 UI (편집 모드) | 비고 |
| ------------- | ------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------- |
| (드래그 핸들) | — | — | `useSortable` 연동 |
| 결재선아이디 | `aplnId` | 읽기 전용 | 신규 행: `—`, 저장 후 서버 `aplnId` 표시 |
| 결재순서번호 | `atrzSeqNo` | 읽기 전용 (그리드 행 순서 1부터 표시) | 저장 시 행 순서 기준 자동 채번 |
| 결재구분 | `atrzSeComCd` | Select (`ATRZSECD` 공통코드) | 표시명 `atrzSeComNm` (조회 행) |
| 결재자 | `aprvrUuid` | 읽기 전용 이름 + **[선택]** 버튼 | 응답에 성명 없음 — 선택 시 로컬 `aprvrNm` 유지 |
| 등록일시 | `regDt` | 읽기 전용 | 신규 행은 `—` |
| 관리 | — | 편집 중인 행: **[저장] [취소]** · 조회 행: **[수정] [삭제]** (다른 행 편집 중이면 비활성) |
- 표시 정렬: 기본 `결재순서번호` 오름차순.
- 임시 행(서버 미저장)과 저장된 행의 시각적 구분 필요(예: ID 컬럼 표기 또는 행 스타일). 세부 UI는 TBD.
- 페이지네이션: TBD.
#### 인라인 편집 정책
| 항목 | 내용 |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| **단일 행 편집** | **행 추가 또는 한 행 수정 중에는 다른 행을 편집할 수 없다.** `editingRowId` 로 현재 편집 행 1건만 허용. |
| 편집 잠금 시 비활성 | 다른 행: 셀·[수정]·[삭제] 비활성 · `[행 추가]` 비활성 · 행 드래그 비활성 · (선택) `[결재선 저장]` 비활성 |
| 편집 해제 | 행별 **[저장]** 성공 또는 **[취소]** 시 `editingRowId` 초기화 후 전체 조회 모드 복귀 |
| 조회 행 | 편집 중이 아닐 때 읽기 전용. **[수정]** 클릭 시 해당 행만 편집 모드 진입 |
| 신규 행 | `[행 추가]` 시 맨 아래 편집 행 1건 생성 (`editingRowId` = 임시 키) |
| 필수값 | `atrzSeComCd`, `aprvrUuid` |
| 순서 중복 | 저장 시 행 순서로 자동 채번하므로 중복 없음 |
| 결재구분 코드 | `ATRZSECD` — 예: `ATRZ01` 검토자, `ATRZ02` 승인자, `ATRZ03` 최종승인자 |
| **순서 제약** | `ATRZ01`은 최상단 블록, `ATRZ03`은 최하단 블록. 드래그 종료·저장 시 `validateAtrzOrder`로 검증, 위반 시 적용/저장 불가 (결재구분 변경 시에는 미검증) |
| 저장 단위 | 행별 **[저장]** (단건 POST/PUT) 또는 툴바 **[결재선 저장]** (일괄) |
#### 행 드래그 정렬 (`@dnd-kit` + `useSortable`)
| 항목 | 내용 |
| ---------- | ------------------------------------------------------------------------------------------------------- |
| 목적 | 결재 단계 순서를 그리드에서 직접 변경 |
| 라이브러리 | `@dnd-kit/core`, `@dnd-kit/sortable` — 테이블 행 POC 후 확정 |
| 저장 시점 | 드래그는 클라이언트 상태만 변경 → **[결재선 저장]** 또는 행별 **[저장]** 시 `atrzSeqNo` PUT |
| 편집 중 | `editingRowKey != null` 이면 드래그 **불가** |
| 순서 제약 | 드래그 종료 시 `validateAtrzOrder` — 실패하면 순서 변경 미적용 + 안내. 결재구분 미선택 행은 드래그 불가 |
### 5-3. 보조 다이얼로그 (검토인력만)
| 다이얼로그명 | 트리거 | 결과 |
| ------------- | ----------------------- | ---------------------------------- |
| 검토인력 선택 | 그리드 결재자 셀 [선택] | `aprvrUuid` 및 UI용 `aprvrNm` 반영 |
> 결재자 **등록·수정 다이얼로그는 사용하지 않음** (v0.2 변경).
---
## 6. API/DTO 명세
> 본 화면은 현재 UI 위주 초안으로, API/DTO는 백엔드 명세 확정 후 채운다.
### 6-1. 공통
| 항목 | 내용 |
| ----------- | ------------------------------------------- |
| Base prefix | TBD (예: `/server` 또는 결재 서비스 prefix) |
| 인증 | JWT 토큰 기반 (Authorization 헤더) |
| 클라이언트 | TBD (예: `@/entities/approval-line`) |
| 항목 | 내용 |
| ----------- | ------------------------------------------------------------- |
| 서비스 | `as-elapr-service` |
| Base prefix | `/as-elapr-service/api/v1` (프록시 경유) |
| 인증 | JWT (`Authorization` 헤더) |
| 페이징 | Spring `Pageable`: `page`, `size`, `sort` → `PageResponse<T>` |
| 엔티티 | `@/entities/elapr` (`elaprTmpltService`, `elaprAplnService`) |
### 6-2. 전자결재서식 (영역 A)
### 6-2. 전자결재서식 (영역 A) — `elapr-tmplt`
| 기능 | Method | URL | Request DTO | Response DTO | API(클라이언트 함수명) |
| ------------------ | ------ | --- | ----------- | ------------ | ---------------------- |
| 결재서식 목록 조회 | TBD | TBD | TBD | TBD | TBD |
| 결재서식 등록 | TBD | TBD | TBD | TBD | TBD |
| 결재서식 수정 | TBD | TBD | TBD | TBD | TBD |
| 결재서식 삭제 | TBD | TBD | TBD | TBD | TBD |
| 기능 | Method | URL | Request / Query | Response | 클라이언트 |
| --------- | ------ | ----------------------------- | ------------------------------------------ | ------------------ | ---------------------------- |
| 목록 조회 | GET | `/elapr-tmplt` | Query: `ElaprTmpltSearchParams` + Pageable | `Page<ElaprTmplt>` | `elaprTmpltService.findPage` |
| 단건 조회 | GET | `/elapr-tmplt/{elaprTmpltId}` | — | `ElaprTmplt` | `elaprTmpltService.findById` |
| 등록 | POST | `/elapr-tmplt` | `ElaprTmpltSaveRequest` | `ElaprTmplt` (201) | `elaprTmpltService.create` |
| 수정 | PUT | `/elapr-tmplt/{elaprTmpltId}` | `ElaprTmpltUpdateRequest` | `ElaprTmplt` | `elaprTmpltService.update` |
| 삭제 | DELETE | `/elapr-tmplt/{elaprTmpltId}` | — | 204 | `elaprTmpltService.remove` |
### 6-3. 결재선 (영역 B)
**ElaprTmplt (응답)**
| 기능 | Method | URL | Request DTO | Response DTO | API(클라이언트 함수명) |
| ------------------------ | ------ | --- | ------------------------------------ | ------------ | ---------------------- |
| 결재선 목록 조회(서식별) | TBD | TBD | `{ elctAtrzTmpltId }` (TBD) | TBD | TBD |
| 결재선 추가/일괄 저장 | TBD | TBD | `{ elctAtrzTmpltId, lines[] }` (TBD) | TBD | TBD |
| 결재선 수정 | TBD | TBD | TBD | TBD | TBD |
| 결재선 삭제 | TBD | TBD | TBD | TBD | TBD |
| Field | 타입 | 설명 |
| ----------------- | ------- | ------------------ |
| `elaprTmpltId` | string | 전자결재서식아이디 |
| `elaprTmpltNm` | string | 결재서식명 |
| `elaprTmpltExpln` | string? | 서식 설명 |
| `docSeComCd` | string? | 문서구분 코드 |
| `docSeComNm` | string? | 문서구분명 |
| `regDt` | string? | 등록일시 |
### 6-4. 검토인력 (다이얼로그)
**ElaprTmpltSaveRequest**: `elaprTmpltNm`\_, `elaprTmpltExpln?`, `docSeComCd?`
| 기능 | Method | URL | Request DTO | Response DTO | API(클라이언트 함수명) |
| ------------------ | ------ | --- | --------------- | ------------ | ---------------------- |
| 검토인력 목록 조회 | TBD | TBD | TBD (검색 필터) | TBD | TBD |
**ElaprTmpltUpdateRequest**: `elaprTmpltNm`\*, `elaprTmpltExpln?`, `docSeComCd?`
**ElaprTmpltSearchParams**: `elaprTmpltNm?`, `docSeComCd?`, `regDtStart?`, `regDtEnd?` + `page`, `size`, `sort`
### 6-3. 결재선 (영역 B) — `elapr-apln`
| 기능 | Method | URL | Request / Query | Response | 클라이언트 |
| --------- | ------ | ---------------------- | ------------------------------- | ----------------- | --------------------------- |
| 목록 조회 | GET | `/elapr-apln` | Query: `elaprTmpltId`, Pageable | `Page<ElaprApln>` | `elaprAplnService.findPage` |
| 단건 조회 | GET | `/elapr-apln/{aplnId}` | — | `ElaprApln` | `elaprAplnService.findById` |
| 등록 | POST | `/elapr-apln` | `ElaprAplnSaveRequest` | `ElaprApln` (201) | `elaprAplnService.create` |
| 수정 | PUT | `/elapr-apln/{aplnId}` | `ElaprAplnUpdateRequest` | `ElaprApln` | `elaprAplnService.update` |
| 삭제 | DELETE | `/elapr-apln/{aplnId}` | — | 204 | `elaprAplnService.remove` |
> **일괄 저장 API 없음** — UI의 [결재선 저장]은 변경된 행에 대해 POST/PUT을 순차(또는 병렬) 호출하는 클라이언트 오케스트레이션으로 구현.
**ElaprApln (응답)**
| Field | 타입 | 설명 |
| -------------- | ------- | ------------------ |
| `aplnId` | string | 결재선아이디 |
| `elaprTmpltId` | string | 전자결재서식아이디 |
| `aprvrUuid` | string? | 결재자 UUID |
| `atrzSeComCd` | string? | 결재구분 코드 |
| `atrzSeComNm` | string? | 결재구분명 |
| `atrzSeqNo` | number? | 결재순서번호 |
| `regDt` | string? | 등록일시 |
**ElaprAplnSaveRequest**: `elaprTmpltId`\_, `aprvrUuid?`, `atrzSeComCd?`, `atrzSeqNo?`
**ElaprAplnUpdateRequest**: `elaprTmpltId`\*, `aprvrUuid?`, `atrzSeComCd?`, `atrzSeqNo?`
### 6-4. 검토인력 (보조)
| 기능 | Method | URL | 비고 |
| ------------------ | ------ | --- | --------------------------- |
| 검토인력 목록 조회 | TBD | TBD | 사용자 서비스 또는 별도 API |
### 6-5. Backend ↔ 화면 필드 매핑 (구 SPEC 대비)
| 구 SPEC 필드 | 백엔드 필드 |
| ----------------- | ---------------------------------------------------- |
| `elctAtrzTmpltId` | `elaprTmpltId` |
| `atrzTmpltNm` | `elaprTmpltNm` |
| `docSeNm` | `docSeComNm` |
| `atrzLineId` | `aplnId` |
| `atrzSeNm` | `atrzSeComNm` |
| `atrzUserNm` | (없음) `aprvrUuid` + 사용자 조회/선택 시 로컬 표시명 |
---
@@ -307,52 +362,46 @@ END
### 7-1. 기술 스택·원칙
- **공통 컴포넌트**: `@common/components` (버튼, 셀렉트, 인풋, 카드 등 기존 패턴 재사용).
- **테이블**: `@tanstack/react-table`, 프로젝트 표준 `useDataTable` 패턴이 있다면 동일하게 적용.
- **데이터**: React Query 기반 조회/뮤테이션. API 확정 후 `queryKey` 와 무효화 정책 정리.
- **상태**: 영역 A의 **선택된 서식 ID**, 영역 B-1의 **결재자 입력 폼 값**, 영역 B-2의 **임시 결재선 배열**은 페이지 로컬 상태로 관리. 다이얼로그 상태(검토인력 선택 등)는 컴포넌트 로컬 상태 또는 가벼운 상태 관리 훅으로 분리.
- **모의 데이터**: 백엔드 명세 확정 전까지 `@faker-js/faker` 로 그리드 목데이터 구성(스크린샷 컬럼/값과 동일 형태).
- **공통 컴포넌트**: `@common/components`
- **테이블**: `@tanstack/react-table` + 인라인 편집 셀 (`Select`, `Input`, `Button`)
- **드래그**: `@dnd-kit` — B-2 행 정렬 (POC 필수)
- **데이터**: React Query — `@/entities/elapr` hooks
- **상태**: 선택된 `elaprTmpltId`, `editingRowId` (단일 행 편집 잠금), B-2 행 배열(dirty·신규·`aprvrNm`·수정 전 스냅샷)
- **모의 데이터**: API 연동 전 `@faker-js/faker`
### 7-2. 모듈·파일 배치(제안)
| 구분 | 경로 |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| 라우트(App Router) | `apps/as/app/(with-base-layout)/approval-line-management/page.tsx` |
| 페이지 컴포넌트 | `apps/as/src/pages/approval-line-management/index.ts`<br/>`.../ui/approval-line-management-page.tsx` |
| 위젯(영역 단위) | `apps/as/src/widgets/approval-template-list/`<br/>`apps/as/src/widgets/approval-line-editor/` |
| 피처(상호작용) | `apps/as/src/features/approver-picker/` (검토인력 선택 다이얼로그) |
| 엔티티(도메인) | `apps/as/src/entities/approval-template/`<br/>`apps/as/src/entities/approval-line/` |
| 그리드 컬럼 정의 | `entities/approval-template/model/approval-template-table-columns.tsx`<br/>`entities/approval-line/model/approval-line-table-columns.tsx` |
| API 클라이언트 | `entities/approval-template/api/*`, `entities/approval-line/api/*` (TBD) |
| 구분 | 경로 |
| ------ | -------------------------------------------------------------------------------------- |
| 라우트 | `apps/as/app/(with-base-layout)/approval-line-management/page.tsx` |
| 페이지 | `apps/as/src/pages/approval-line-management/` (Public API: page만 export) |
| UI | `.../ui/elapr-tmplt-list.tsx`, `elapr-tmplt-form-dialog.tsx`, `elapr-apln-grid.tsx` 등 |
| model | `.../model/columns.tsx`, `apln-row-types.ts`, `mock-reviewers.ts` |
| lib | `.../lib/use-apln-rows.ts`, `validators.ts`, `use-dnd-sensors.ts` |
| 엔티티 | `apps/as/src/entities/elapr/` |
### 7-3. 영역별 UI 매핑
| 영역 | 주요 UI | 비고 |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
| A | 섹션 헤더 + Button(`서식 등록`) + DataTable (행 선택, 행별 Action 버튼) | 선택 행 강조, 행 클릭으로 영역 B 갱신 |
| B 헤더 | 상태 라벨 `선택된 서식: {ID}` | 미선택 시 영역 B-1·B-2 비활성 |
| B-1 | 읽기 전용 Input(아이디), Select(결재구분), Input+Button(결재자/검토인력 선택), NumberInput(순서번호), Action 버튼 3종(`결재자 추가`/`결재선 저장`/`초기화`) | 폼 유효성 검증 후 그리드에 임시 행 추가 |
| B-2 | DataTable (행별 [수정][삭제]), 임시 행 시각적 구분 | 기본 정렬: 결재순서번호 ASC |
| 공통 | Dialog(검토인력 선택, 서식 등록/수정, 결재자 수정, 삭제 확인) | 디자인 시스템의 `Dialog` 컴포넌트 사용 |
| 영역 | 주요 UI |
| ------ | -------------------------------------------------------------------------------------------- |
| A | DataTable + 행 선택 + 서식 등록/수정 다이얼로그 |
| B 헤더 | 상태 라벨 + `행 추가`, `결재선 저장` |
| B-2 | 인라인 편집 DataTable + `editingRowId` 단일 행 잠금 + 드래그 핸들 + 행별 수정/저장/삭제/취소 |
| 공통 | 서식 Dialog, 검토인력 선택 Dialog만 |
### 7-4. 데이터 흐름(요약)
- **진입 시**: 결재서식 목록 조회(TBD) → 영역 A 그리드 렌더. 영역 B는 안내/비활성 상태.
- **서식 선택 시**: 선택된 `elctAtrzTmpltId` 를 페이지 상태로 보관 → 결재선 목록 조회(TBD) → 영역 B-2 그리드 갱신. 영역 B-1 폼 초기화.
- **결재자 추가**: 입력 폼 값을 영역 B-2의 **클라이언트 임시 배열**에 push. 결재선아이디는 미발급 상태로 표시(또는 임시 키). 폼 리셋.
- **결재선 저장**: 임시 배열 + 기존 그리드 행을 합쳐 일괄 저장 API(TBD) 호출 → 성공 시 그리드 재조회 + 토스트.
- **수정/삭제**: 각각의 단건 API(TBD) 호출 후 그리드 재조회.
- **검토인력 선택 다이얼로그**: 사용자 목록 조회(TBD) → 단일 선택 → 폼의 결재자명/결재자 ID 갱신.
- **서식 등록/수정/삭제**: 다이얼로그 또는 확인 후 단건 API(TBD) 호출 → 영역 A 그리드 재조회. 삭제 대상이 현재 선택된 서식이면 영역 B 초기화.
- **진입**: `useElaprTmpltPage` → 영역 A. B 비활성.
- **서식 선택**: `useElaprAplnPage({ elaprTmpltId })` → B-2.
- **행 추가**: `editingRowId` 없을 때만 가능 → 신규 행 1건 편집 잠금 → 저장/취소 후 해제.
- **인라인 수정**: [수정]으로 `editingRowId` 설정 → 해당 행만 편집 → `useUpdateElaprApln` 후 해제.
- **드래그**: `editingRowId == null` 일 때만 가능 → 로컬 `atrzSeqNo` 재부여 → 일괄 PUT.
- **삭제**: `useDeleteElaprApln` 또는 신규 행 로컬 제거.
### 7-5. 미확정 / 검증 필요 항목 (TBD)
### 7-5. 미확정 / 검증 필요
- 모든 백엔드 API 경로·DTO·필드명.
- `결재선아이디(자동발급)` 입력 박스의 표시 정책: (a) 다음 발급 예정 번호 미리 보여주기, (b) 저장 전에는 빈 칸 또는 자리표시자만, (c) 임시 ID.
- `[결재자 추가]` 의 동작: 그리드에 임시 누적(현재 가정) vs 즉시 단건 저장.
- 결재구분 코드 값(`접수`/`검토`/`최종승인`)의 코드 매핑.
- 문서구분 코드 값(`설치인가`/`기본계획` 등)의 코드 매핑.
- 영역 A·B 그리드의 페이지네이션·정렬·검색 필터 유무.
- 검토인력 선택 다이얼로그의 검색 필드(부서/직급/이름 등) 및 다중 선택 허용 여부.
- 결재선 저장 시 순서번호 중복·누락(예: 1, 2, 4) 허용 정책.
- 권한(역할별 등록/수정/삭제 가능 여부).
- 검토인력 API 경로·DTO.
- `@dnd-kit` + react-table 행 인라인 Input 동시 적용 POC.
- 결재자 성명 표시: `aprvrUuid` → 사용자명 조회 API.
- 영역 A·B 페이지네이션·검색 UI 노출 여부.
- 권한(역할별 CRUD).
+487
View File
@@ -0,0 +1,487 @@
# 화면설계서: 체크리스트관리
## 1. 기본 정보
| 항목 | 내용 |
| ---------- | -------------------------------------------- |
| 버전 | 0.1 |
| 프로세스명 | UI설계 |
| 화면명 | 체크리스트관리 |
| 화면 유형 | [v] 조회 [v] 입력 [v] 수정 [v] 삭제 [ ] 출력 |
| 메뉴 경로 | 디지털행정지원 > 시스템관리 > 체크리스트관리 |
---
## 2. 화면 목적
업무별·문서별 **점검표**(체크리스트)를 등록·관리하고, **점검항목** 마스터를 유지한다. 상단 좌측에서 점검표를 선택하면 하단 **체크리스트 항목** 영역에 해당 점검표의 검토 구성(계층형 검토내용)을 편집한다. 상단 우측 **점검항목 관리**는 점검표에 연결할 항목 풀을 등록·수정·삭제한다.
---
## 3. 화면 구성
### 3-1. 전체 레이아웃
```
┌───────────────────────────────────────────────────────────────────────────────────────────────┐
│ 페이지 타이틀: 체크리스트관리 브레드크럼: 디지털행정지원 > … > │
├──────────────────────────────────────────────┬────────────────────────────────────────────────┤
│ [영역 A] 점검표 관리 │ [영역 B] 점검항목 관리 │
│ [점검표 추가] │ [점검항목 추가] │
│ ┌────────────────────────────────────────┐ │ ┌──────────────────────────────────────────┐ │
│ │ 점검표일련번호 | 점검표명 | 문서구분 │ │ │ 점검항목코드 | 점검항목명 | 사용여부 │ │
│ │ 업무구분 | 사용여부 | 등록일시 | 관리 │ │ │ 등록일시 | 관리 │ │
│ ├────────────────────────────────────────┤ │ ├──────────────────────────────────────────┤ │
│ │ 1 (선택 행 강조) [관리] [삭제] │ │ │ ITEM_001 요건검토 … [관리][삭제] │ │
│ │ │ │ │ ITEM_002 법령검토 … [관리][삭제] │ │
│ └────────────────────────────────────────┘ │ └──────────────────────────────────────────┘ │
├──────────────────────────────────────────────┴────────────────────────────────────────────────┤
│ [영역 C] 체크리스트 항목 [미리보기] [추가] [저장] │
│ ┌─────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ 점검항목 ▼ | 주요 검토내용 | 사용 여부 | 구분 | 관리 │ │
│ │ L1 [요건검토 ▼] | (입력) | ○예 ○아니오 | 1단계 | [하위추가]│ │
│ │ L2 [요건검토 ▼] | (입력) | ○예 ○아니오 | 2단계 | [삭제] │ │
│ └─────────────────────────────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────────────────────────────┘
```
### 3-2. 화면 데이터 흐름
```
BEGIN
// 최초 진입
CALL GET /as-checklist-service/api/v1/cklst
DISPLAY 점검표 그리드 (영역 A)
CALL GET /as-checklist-service/api/v1/cklst-artcl
DISPLAY 점검항목 그리드 (영역 B)
DISPLAY 빈 상태 안내 (영역 C) // 점검표 미선택 시 C 비활성
// 영역 A: 점검표 행 선택
ON 영역 A 행 CLICK DO
SET 선택된 점검표 = 행.cklstSn
CALL GET /as-checklist-service/api/v1/cklst-rvw?cklstSn={cklstSn}
// TBD: 점검표 단위 전체 검토 트리 조회 API — 현재는 항목별 by-artcl 조합 또는 클라이언트 병합
DISPLAY 체크리스트 항목 그리드 (영역 C)
END ON
// 영역 A: 점검표 추가
ON [점검표 추가] 버튼 CLICK DO
OPEN 점검표 추가 다이얼로그
ON [저장] DO
VALIDATE cklstNm, taskSeComCd
CALL POST /as-checklist-service/api/v1/cklst
CALL GET /as-checklist-service/api/v1/cklst
CLOSE 다이얼로그
END ON
ON [취소] / [닫기] DO
CLOSE 다이얼로그
END ON
END ON
// 영역 A: 점검표 수정·삭제
ON 영역 A 행 [관리] CLICK DO
OPEN 점검표 수정 다이얼로그 (기존 값 초기화)
ON [저장] DO
CALL PUT /as-checklist-service/api/v1/cklst/{cklstSn}
CALL GET /as-checklist-service/api/v1/cklst
END ON
END ON
ON 영역 A 행 [삭제] CLICK DO
CONFIRM "선택한 점검표를 삭제하시겠습니까?"
CALL DELETE /as-checklist-service/api/v1/cklst/{cklstSn}
IF 삭제 대상 == 선택된 점검표 THEN RESET 영역 C
CALL GET /as-checklist-service/api/v1/cklst
END ON
// 영역 B: 점검항목 추가
ON [점검항목 추가] 버튼 CLICK DO
OPEN 점검항목 추가 다이얼로그
ON [저장] DO
VALIDATE cklstArtclNm
// TBD: cklstSn 필수 — UI에 소속 점검표 선택 없음. 백엔드 정책 확인
CALL POST /as-checklist-service/api/v1/cklst-artcl
CALL GET /as-checklist-service/api/v1/cklst-artcl
CLOSE 다이얼로그
END ON
END ON
// 영역 B: 점검항목 수정·삭제
ON 영역 B 행 [관리] CLICK DO
OPEN 점검항목 수정 다이얼로그
ON [저장] DO
CALL PUT /as-checklist-service/api/v1/cklst-artcl/{cklstArtclSn}/{cklstSn}
END ON
END ON
ON 영역 B 행 [삭제] CLICK DO
CONFIRM "선택한 점검항목을 삭제하시겠습니까?"
CALL DELETE /as-checklist-service/api/v1/cklst-artcl/{cklstArtclSn}/{cklstSn}
CALL GET /as-checklist-service/api/v1/cklst-artcl
END ON
// 영역 C: 검토 행 추가
ON [추가] 버튼 CLICK DO
VALIDATE 선택된 점검표 존재
ADD 영역 C 그리드 맨 아래 편집 가능 행 (1단계, rvwCnLvNo=1)
END ON
// 영역 C: 하위 검토 추가
ON 행 [하위추가] CLICK DO
ADD 부모행 하위 편집 행 (2단계, upCklstRvwSn=부모.cklstRvwSn, rvwCnLvNo=2)
END ON
// 영역 C: 일괄 저장
ON [저장] 버튼 CLICK DO
VALIDATE 선택된 점검표 존재
FOR EACH 신규·변경된 검토 행 DO
IF 신규 THEN
CALL POST /as-checklist-service/api/v1/cklst-rvw
ELSE
CALL PUT /as-checklist-service/api/v1/cklst-rvw/{cklstRvwSn}/{cklstArtclSn}/{cklstSn}
END IF
END FOR
CALL GET /as-checklist-service/api/v1/cklst-rvw?cklstSn={cklstSn}
TOAST "체크리스트 항목이 저장되었습니다."
END ON
// 영역 C: 행 삭제
ON 행 [삭제] CLICK DO
CONFIRM "선택한 항목을 삭제하시겠습니까?"
IF 서버 저장된 행 THEN
CALL DELETE /as-checklist-service/api/v1/cklst-rvw/{cklstRvwSn}/{cklstArtclSn}/{cklstSn}
END IF
REMOVE 그리드 행 (미저장 행은 API 호출 없이 제거)
END ON
// 영역 C: 미리보기
ON [미리보기] 버튼 CLICK DO
OPEN 체크리스트 미리보기 다이얼로그 (읽기 전용 트리)
// TBD: 미리보기 UI·API
END ON
END
```
---
## 4. 영역 A: 점검표 관리
선택된 점검표가 영역 C의 편집 대상이 된다. 행 클릭으로 선택 상태를 표시한다.
### 4-1. 헤더 / 툴바
| 항목 | 내용 |
| ----------- | ------------------------------------------ |
| 섹션 타이틀 | 점검표 관리 |
| 우측 버튼 | **[점검표 추가]** — 점검표 등록 다이얼로그 |
### 4-2. 그리드 컬럼
| 컬럼명 | Field | 타입 | 비고 |
| ---------------- | ------------- | ------ | ---------------------------------------------------- |
| 점검표일련번호 | `cklstSn` | number | PK |
| 점검표명 | `cklstNm` | string | |
| 문서구분공통코드 | `docSeComCd` | string | 코드명 표시 — `ASDOCSECD` 등 공통코드 (TBD 상위코드) |
| 업무구분공통코드 | `taskSeComCd` | string | RE/SF/SP 등 — 공통코드 (TBD 상위코드) |
| 사용여부 | `useYn` | string | `Y`→예, `N`→아니오 |
| 등록일시 | `regDt` | string | `yyyy-MM-dd HH:mm:ss` |
| 관리 | — | — | 행별 **[관리] · [삭제]** |
- 행 클릭 시: `cklstSn` 선택 → 영역 C 헤더·그리드 갱신.
- 페이지네이션: Spring `Pageable` (`page`, `size`, `sort`).
### 4-3. 버튼
| 버튼명 | 위치 | 동작 |
| ----------- | -------------- | ------------------------------------------------- |
| 점검표 추가 | 섹션 우측 상단 | 점검표 추가 다이얼로그 오픈 |
| 관리 | 행별 | 점검표 수정 다이얼로그 (기존 값 초기화) |
| 삭제 | 행별 | 확인 후 삭제. 선택 중인 점검표이면 영역 C 초기화. |
### 4-4. 다이얼로그: 점검표 추가 / 수정
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
| -------- | ---------------- | ---- | ------ | ----------------- | ------------------------------------------------ |
| 점검표명 | Text | Y | — | `cklstNm` | placeholder: "예: 설치인가 체크리스트" |
| 문서구분 | Select(공통코드) | N | — | `docSeComCd` | 예: FNRS, INSTL, PWRK, DESIGN |
| 업무구분 | Select(공통코드) | Y | — | `taskSeComCd` | 예: RE(재이용수), SF(하수처리시설), SP(하수관로) |
| 사용여부 | Select | N | Y | `useYn` | Y/N, UI 표시: 예/아니오 |
| 버튼명 | 동작 |
| ------ | -------------------------------------- |
| 저장 | POST(등록) 또는 PUT(수정) 후 목록 갱신 |
| 취소 | 다이얼로그 닫기, 입력값 폐기 |
| 닫기 | 다이얼로그 닫기 |
---
## 5. 영역 B: 점검항목 관리
점검표 하단(영역 C)에서 선택할 **점검항목** 마스터를 관리한다.
### 5-1. 헤더 / 툴바
| 항목 | 내용 |
| ----------- | ---------------------------------------------- |
| 섹션 타이틀 | 점검항목 관리 |
| 우측 버튼 | **[점검항목 추가]** — 점검항목 등록 다이얼로그 |
### 5-2. 그리드 컬럼
| 컬럼명 | Field | 타입 | 비고 |
| ------------ | -------------- | ------ | ------------------------------------- |
| 점검항목코드 | `cklstArtclSn` | number | UI mock `ITEM_001` 형식 표시 여부 TBD |
| 점검항목명 | `cklstArtclNm` | string | |
| 사용여부 | `useYn` | string | `Y`→예, `N`→아니오 |
| 등록일시 | `regDt` | string | `yyyy-MM-dd HH:mm:ss` |
| 관리 | — | — | 행별 **[관리] · [삭제]** |
> **TBD**: 백엔드 `CklstArtcl`은 복합 PK(`cklstArtclSn`, `cklstSn`)이며 등록 시 `cklstSn` 필수. UI 다이얼로그에는 `cklstSn` 입력이 없음 — 소속 점검표 지정 방식·전역 마스터 여부 백엔드 정책 확인.
### 5-3. 다이얼로그: 점검항목 추가 / 수정
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
| ---------- | --------- | ---- | ------ | ----------------- | --------------------------- |
| 점검항목명 | Text | Y | — | `cklstArtclNm` | placeholder: "예: 법령검토" |
| 사용여부 | Select | N | Y | `useYn` | Y/N |
| 버튼명 | 동작 |
| ------ | -------------------------------------- |
| 저장 | POST(등록) 또는 PUT(수정) 후 목록 갱신 |
| 취소 | 다이얼로그 닫기 |
| 닫기 | 다이얼로그 닫기 |
---
## 6. 영역 C: 체크리스트 항목 (선택된 점검표)
영역 A에서 선택된 `cklstSn` 기준으로 **점검표검토**(계층형 검토내용)를 구성한다.
### 6-1. 헤더 / 툴바
| 항목 | 내용 |
| ----------- | -------------------------------------------------------------------- |
| 섹션 타이틀 | 체크리스트 항목 (선택된 점검표: {cklstNm}) |
| 우측 버튼 | **[미리보기]** · **[추가]** · **[저장]** — 점검표 미선택 시 C 비활성 |
### 6-2. 그리드 컬럼
| 컬럼명 | Field | 입력 UI | 비고 |
| ------------- | -------------- | ------------------------------------- | ------------------------------------ |
| 점검항목 | `cklstArtclSn` | Select (영역 B 항목 목록) | L1/L2 접두 표시, `cklstArtclNm` 라벨 |
| 주요 검토내용 | `rvwCn` | Text / Textarea | 최대 4000자 |
| 사용 여부 | `useYn` | Radio (예/아니오) | `Y` / `N` |
| 구분 | `rvwCnLvNo` | 읽기 전용 | 1→1단계, 2→2단계, 3→3단계 |
| 관리 | — | **[하위추가]** (1·2단계, 저장 후) · **[삭제]** | 3단계 행은 [하위추가] 미표시 |
#### 계층 정책
| 항목 | 내용 |
| -------------- | -------------------------------------------------------------------- |
| 최대 깊이 | 3단계 (`rvwCnLvNo`: 1, 2, 3) |
| 상위-하위 연결 | `upCklstRvwSn` — 2단계 행은 부모 1단계 `cklstRvwSn` 참조 |
| 정렬 | `sortSeq` — 동일 레벨 내 순서 (TBD: UI 드래그 정렬 여부) |
| 체크 사용 | `bchckUseYn` — 미리보기·실사용 화면 체크박스 노출 여부 (TBD UI 반영) |
### 6-3. 버튼
| 버튼명 | 동작 |
| -------- | ---------------------------------------------------- |
| 추가 | 1단계 검토 행 1건 추가 (편집 가능) |
| 저장 | 변경·신규 검토 행 일괄 POST/PUT (일괄 저장 API 없음) |
| 미리보기 | 선택 점검표 체크리스트 읽기 전용 미리보기 (TBD) |
| 하위추가 | 선택 1·2단계 행 아래 하위 단계(2·3단계) 자식 행 추가 |
| 삭제 | 확인 후 DELETE 또는 미저장 행 로컬 제거 |
---
## 7. API/DTO 명세
### 7-1. 공통
| 항목 | 내용 |
| ------------- | ------------------------------------------------------------- |
| 서비스 | `as-checklist-service` |
| Base prefix | `/as-checklist-service/api/v1` (게이트웨이 프록시 경유) |
| 인증 | JWT (`Authorization` 헤더), `X-Site-Id`, `token-id` |
| 페이징 | Spring `Pageable`: `page`, `size`, `sort` → `PageResponse<T>` |
| 엔티티 | `@/entities/cklst` (`cklstMngService`, `cklstArtclService`, `cklstRvwService`) |
### 7-2. 점검표 (영역 A) — `cklst`
| 기능 | Method | URL | Request / Query | Response | 클라이언트 |
| --------- | ------ | ------------------ | --------------------------------- | ------------------ | -------------------------- |
| 목록 조회 | GET | `/cklst` | `CklstMngSearchParams` + Pageable | `Page<CklstMng>` | `cklstMngService.findPage` |
| 단건 조회 | GET | `/cklst/{cklstSn}` | — | `CklstMng` | `cklstMngService.findById` |
| 등록 | POST | `/cklst` | `CklstMngSaveRequest` | `number` (cklstSn) | `cklstMngService.create` |
| 수정 | PUT | `/cklst/{cklstSn}` | `CklstMngSaveRequest` | `number` (cklstSn) | `cklstMngService.update` |
| 삭제 | DELETE | `/cklst/{cklstSn}` | — | `number` (cklstSn) | `cklstMngService.remove` |
**CklstMng (응답)**
| Field | 타입 | 설명 |
| ------------- | ------- | ---------------- |
| `cklstSn` | number | 점검표일련번호 |
| `cklstNm` | string | 점검표명 |
| `taskSeComCd` | string | 업무구분공통코드 |
| `useYn` | string? | 사용여부 |
| `docSeComCd` | string? | 문서구분공통코드 |
| `rgtr` | string? | 등록자 UUID |
| `rgtrNm` | string? | 등록자명 |
| `mdfr` | string? | 수정자 UUID |
| `regDt` | string? | 등록일시 |
| `mdfcnDt` | string? | 수정일시 |
**CklstMngSaveRequest**: `cklstNm`\*, `taskSeComCd`\*, `useYn?`, `docSeComCd?`
**CklstMngSearchParams**: `cklstNm?`, `taskSeComCd?`, `useYn?`, `docSeComCd?` + `page`, `size`, `sort`
**React Query hooks**: `useCklstMngPage`, `useCklstMng`, `useCreateCklstMng`, `useUpdateCklstMng`, `useDeleteCklstMng`
### 7-3. 점검표항목 (영역 B) — `cklst-artcl`
| 기능 | Method | URL | Request / Query | Response | 클라이언트 |
| ------------------ | ------ | --------------------------------------- | ----------------------------------- | ------------------ | --------------------------------- |
| 목록 조회 (페이징) | GET | `/cklst-artcl` | `CklstArtclSearchParams` + Pageable | `Page<CklstArtcl>` | `cklstArtclService.findPage` |
| 점검표별 전체 목록 | GET | `/cklst-artcl/by-cklst/{cklstSn}` | — | `CklstArtcl[]` | `cklstArtclService.findByCklstSn` |
| 단건 조회 | GET | `/cklst-artcl/{cklstArtclSn}/{cklstSn}` | — | `CklstArtcl` | `cklstArtclService.findById` |
| 등록 | POST | `/cklst-artcl` | `CklstArtclSaveRequest` | `CklstArtclId` | `cklstArtclService.create` |
| 수정 | PUT | `/cklst-artcl/{cklstArtclSn}/{cklstSn}` | `CklstArtclUpdateRequest` | `CklstArtclId` | `cklstArtclService.update` |
| 삭제 | DELETE | `/cklst-artcl/{cklstArtclSn}/{cklstSn}` | — | `CklstArtclId` | `cklstArtclService.remove` |
**CklstArtcl (응답)**
| Field | 타입 | 설명 |
| ----------------- | ------- | ------------------ |
| `cklstArtclSn` | number | 점검표항목일련번호 |
| `cklstSn` | number | 점검표일련번호 |
| `cklstArtclNm` | string? | 점검표항목명 |
| `useYn` | string? | 사용여부 |
| `rgtr` / `rgtrNm` | string? | 등록자 |
| `regDt` | string? | 등록일시 |
**CklstArtclSaveRequest**: `cklstSn`\*, `cklstArtclNm?`, `useYn?`
**CklstArtclUpdateRequest**: `cklstArtclNm?`, `useYn?`
**CklstArtclId**: `{ cklstArtclSn: number; cklstSn: number }`
**React Query hooks**: `useCklstArtclPage`, `useCklstArtclList`, `useCklstArtcl`, `useCreateCklstArtcl`, `useUpdateCklstArtcl`, `useDeleteCklstArtcl`
### 7-4. 점검표검토 (영역 C) — `cklst-rvw`
| 기능 | Method | URL | Request / Query | Response | 클라이언트 |
| --------------------- | ------ | -------------------------------------------------- | ------------------------------ | ---------------- | ------------------------------- |
| 목록 조회 (페이징) | GET | `/cklst-rvw` | `CklstRvwSearchParams` + Pageable | `Page<CklstRvw>` | `cklstRvwService.findPage` |
| 항목별 검토 전체 목록 | GET | `/cklst-rvw/by-artcl/{cklstSn}/{cklstArtclSn}` | — | `CklstRvw[]` | `cklstRvwService.findByArtcl` |
| 단건 조회 | GET | `/cklst-rvw/{cklstRvwSn}/{cklstArtclSn}/{cklstSn}` | — | `CklstRvw` | `cklstRvwService.findById` |
| 등록 | POST | `/cklst-rvw` | `CklstRvwSaveRequest` | `CklstRvwId` | `cklstRvwService.create` |
| 수정 | PUT | `/cklst-rvw/{cklstRvwSn}/{cklstArtclSn}/{cklstSn}` | `CklstRvwUpdateRequest` | `CklstRvwId` | `cklstRvwService.update` |
| 삭제 | DELETE | `/cklst-rvw/{cklstRvwSn}/{cklstArtclSn}/{cklstSn}` | — | `CklstRvwId` | `cklstRvwService.remove` |
**CklstRvw (응답)**
| Field | 타입 | 설명 |
| -------------- | ------- | ---------------------- |
| `cklstRvwSn` | number | 점검표검토일련번호 |
| `cklstArtclSn` | number | 점검표항목일련번호 |
| `cklstSn` | number | 점검표일련번호 |
| `upCklstRvwSn` | number? | 상위점검표검토일련번호 |
| `rvwCnLvNo` | number? | 검토내용레벨번호 |
| `rvwCn` | string? | 검토내용 |
| `bchckUseYn` | string? | 체크사용여부 |
| `sortSeq` | number? | 정렬순서 |
| `useYn` | string? | 사용여부 |
| `regDt` | string? | 등록일시 |
**CklstRvwSaveRequest**: `cklstArtclSn`\*, `cklstSn`\*, `upCklstRvwSn?`, `rvwCnLvNo?`, `rvwCn?`, `bchckUseYn?`, `sortSeq?`, `useYn?`
**CklstRvwUpdateRequest**: `upCklstRvwSn?`, `rvwCnLvNo?`, `rvwCn?`, `bchckUseYn?`, `sortSeq?`, `useYn?`
**CklstRvwSearchParams**: `cklstSn?`, `cklstArtclSn?`, `useYn?` + `page`, `size`, `sort`
**CklstRvwId**: `{ cklstRvwSn: number; cklstArtclSn: number; cklstSn: number }`
**React Query hooks**: `useCklstRvwPage`, `useCklstRvwByArtcl`, `useCklstRvw`, `useCreateCklstRvw`, `useUpdateCklstRvw`, `useDeleteCklstRvw`
### 7-5. 공통코드 (보조)
| UI 필드 | 코드 그룹 (TBD) | 예시 값 |
| -------- | --------------- | -------------------------------------------- |
| 문서구분 | `ASDOCSECD` 등 | FNRS, INSTL, PWRK, DESIGN |
| 업무구분 | TBD | RE(재이용수), SF(하수처리시설), SP(하수관로) |
| 사용여부 | 고정 | Y, N |
공통코드 조회: `@/entities/code` — `codeService`, `CodeSelect`
---
## 8. UI 구현 가이드
### 8-1. 기술 스택·원칙
- **공통 컴포넌트**: `@common/components` (`Dialog`, `Button`, `Input`, `Select`, `RadioGroup` 등)
- **테이블**: `@tanstack/react-table` + `useDataTable`
- **데이터**: React Query — `@/entities/cklst` hooks (`useCklstRvwPage` 등 영역 C 포함)
- **공통코드**: `@/entities/code` — `CodeSelect`, `findCodeLabel`
- **상태**: 선택된 `cklstSn`, 영역 C 검토 행 배열(신규·dirty·계층 트리)
- **모의 데이터**: API 연동 전 `@faker-js/faker`
### 8-2. 모듈·파일 배치(제안)
| 구분 | 경로 |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 라우트 | `apps/as/app/(with-base-layout)/checklist-management/page.tsx` |
| 페이지 | `apps/as/src/pages/checklist-management/` (Public API: page만 export) |
| UI | `.../ui/cklst-mng-list.tsx`, `cklst-mng-form-dialog.tsx`, `cklst-artcl-list.tsx`, `cklst-artcl-form-dialog.tsx`, `cklst-rvw-grid.tsx`, `cklst-preview-dialog.tsx` |
| model | `.../model/cklst-mng-columns.tsx`, `cklst-artcl-columns.tsx`, `cklst-rvw-row-types.ts` |
| lib | `.../lib/use-cklst-rvw-rows.ts`, `validators.ts`, `build-rvw-tree.ts` |
| 엔티티 | `apps/as/src/entities/cklst/` (`cklstMng`, `cklstArtcl`, `cklstRvw` 서비스·hooks) |
| 페이지 | `apps/as/src/pages/checklist-management/` — `/checklist-management` |
| 문서 | `apps/as/docs/checklist-management/SPEC.md` |
### 8-3. 영역별 UI 매핑
| 영역 | 주요 UI |
| ---- | ---------------------------------------------------------------------------- |
| A | DataTable + 행 선택 + 점검표 추가/수정 Dialog |
| B | DataTable + 점검항목 추가/수정 Dialog |
| C | 계층형 편집 그리드(1·2단계) + [추가][저장][미리보기] + 행별 [하위추가][삭제] |
| 공통 | `CodeSelect` (문서구분·업무구분), 사용여부 Y/N Select/Radio |
### 8-4. 다이얼로그·라우팅
| 다이얼로그명 | 트리거 | API 연동 |
| ------------------- | --------------- | ------------------------- |
| 점검표 추가 | [점검표 추가] | `useCreateCklstMng` |
| 점검표 수정 | 행 [관리] | `useUpdateCklstMng` |
| 점검표 삭제 확인 | 행 [삭제] | `useDeleteCklstMng` |
| 점검항목 추가 | [점검항목 추가] | `useCreateCklstArtcl` |
| 점검항목 수정 | 행 [관리] | `useUpdateCklstArtcl` |
| 체크리스트 미리보기 | [미리보기] | TBD — 읽기 전용 트리 렌더 |
라우트: `/checklist-management` (메뉴 URL은 CMS 등록값과 일치시킴)
### 8-5. 데이터 흐름(요약)
- **진입**: `useCklstMngPage` → 영역 A, `useCklstArtclPage` → 영역 B. C 비활성.
- **점검표 선택**: `cklstSn` 설정 → 영역 C 활성 → `useCklstRvwPage({ cklstSn })` → 트리 그리드 표시.
- **점검표 CRUD**: Dialog → `useCreateCklstMng` / `useUpdateCklstMng` / `useDeleteCklstMng`.
- **점검항목 CRUD**: Dialog → `useCreateCklstArtcl` / `useUpdateCklstArtcl` / `useDeleteCklstArtcl`.
- **검토 저장**: [저장] → 신규 POST / 기존 PUT 순차 호출 (일괄 API 없음).
- **영역 C 점검항목 Select**: `useCklstArtclList(cklstSn)` 또는 영역 B 전체 목록과 교차 — **TBD**.
### 8-6. 미확정 / 검증 필요
- [ ] 영역 B 점검항목 등록 시 `cklstSn` 필수 vs UI 무소속 마스터 — 백엔드·기획 확인
- [ ] `점검항목코드`(ITEM_001) 표시 형식 vs `cklstArtclSn` 숫자 PK
- [ ] 영역 C 전체 검토 트리 조회: `cklstSn` 단위 일괄 API 부재 — `by-artcl` 다건 호출 또는 백엔드 확장
- [x] `@/entities/cklst` — `cklstRvwService`·hooks 구현
- [x] `pages/checklist-management` — 영역 A·B·C UI 구현 (`/checklist-management`)
- [ ] 문서구분·업무구분 공통코드 상위코드 ID (`ASDOCSECD` 등)
- [ ] [미리보기] 다이얼로그 UI·데이터 소스
- [ ] `bchckUseYn` UI 반영 여부
- [ ] 영역 A·B 페이지네이션·검색 UI 노출 여부
- [ ] 권한(역할별 CRUD)
+385
View File
@@ -0,0 +1,385 @@
# 화면설계서: 코드관리
## 1. 기본 정보
| 항목 | 내용 |
| ---------- | -------------------------------------------- |
| 버전 | 0.1 |
| 프로세스명 | UI설계 |
| 화면명 | 코드관리 |
| 화면 유형 | [v] 조회 [v] 입력 [v] 수정 [v] 삭제 [ ] 출력 |
| 메뉴 경로 | 디지털행정지원 > 사이트관리 > 코드관리 |
---
## 2. 화면 목적
설치인가 등 AS 업무에서 참조하는 **공종구분코드**(공종코드)와 **회계구분코드**(회계코드)를 한 화면에서 조회·등록·수정·삭제한다. 좌측 **공종코드 관리**와 우측 **회계코드 관리**는 독립된 목록·CRUD 영역이며, 각 섹션은 자체 API(`cnstp-se`, `acntg-se`)와 연동한다.
---
## 3. 화면 구성
### 3-1. 전체 레이아웃
```
┌───────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 페이지 타이틀: 코드관리 브레드크럼: 디지털행정지원 > 사이트관리 > 코드관리 │
├──────────────────────────────────────────────────┬────────────────────────────────────────────────┤
│ [영역 A] 공종코드 관리 │ [영역 B] 회계코드 관리 │
│ [등록] │ [등록] │
│ ┌────────────────────────────────────────────┐ │ ┌──────────────────────────────────────────┐ │
│ │ 코드 | 코드명 | 설명 | 사용여부 | 관리 │ │ │ 코드 | 코드명 | 설명 | 사용여부 | 관리 │ │
│ ├────────────────────────────────────────────┤ │ ├──────────────────────────────────────────┤ │
│ │ WC001 공공하수처리시설 … 사용 [수정][삭제]│ │ │ AC101 시설비 … 사용 [수정][삭제] │ │
│ │ WC002 하수관로정비 … 사용 [수정][삭제]│ │ │ AC102 용역비 … 사용 [수정][삭제] │ │
│ │ WC003 재이용시설 … 사용 [수정][삭제]│ │ │ AC103 운영비 … 미사용 [수정][삭제] │ │
│ └────────────────────────────────────────────┘ │ └──────────────────────────────────────────┘ │
│ « 1 2 3 … » │ « 1 2 3 … » │
└──────────────────────────────────────────────────┴────────────────────────────────────────────────┘
```
### 3-2. 화면 데이터 흐름
```
BEGIN
// 최초 진입 — 좌·우 독립 조회
CALL cnstpSeMngService.findAll({ page, size })
DISPLAY 공종코드 그리드 (영역 A)
CALL acntgSeMngService.findAll({ page, size })
DISPLAY 회계코드 그리드 (영역 B)
// 영역 A: 공종코드 등록
ON 영역 A [등록] 버튼 CLICK DO
OPEN 공종코드 등록 다이얼로그
ON [저장] DO
VALIDATE cnstpSeCd
CALL cnstpSeMngService.save(CnstpSeMngSaveRequestDto)
CALL cnstpSeMngService.findAll
CLOSE 다이얼로그
END ON
ON [취소] / [닫기] DO
CLOSE 다이얼로그
END ON
END ON
// 영역 A: 공종코드 수정
ON 영역 A 행 [수정] CLICK DO
CALL cnstpSeMngService.findById(cnstpSeCd)
OPEN 공종코드 수정 다이얼로그 (기존 값 초기화)
ON [저장] DO
CALL cnstpSeMngService.update(cnstpSeCd, CnstpSeMngSaveRequestDto)
CALL cnstpSeMngService.findAll
CLOSE 다이얼로그
END ON
END ON
// 영역 A: 공종코드 삭제
ON 영역 A 행 [삭제] CLICK DO
CONFIRM "선택한 공종코드를 삭제하시겠습니까?"
CALL cnstpSeMngService.delete(cnstpSeCd)
CALL cnstpSeMngService.findAll
END ON
// 영역 B: 회계코드 등록
ON 영역 B [등록] 버튼 CLICK DO
OPEN 회계코드 등록 다이얼로그
ON [저장] DO
VALIDATE acntgSeCd, acntgSeCdNm
CALL acntgSeMngService.save(AcntgSeMngSaveRequestDto)
CALL acntgSeMngService.findAll
CLOSE 다이얼로그
END ON
ON [취소] / [닫기] DO
CLOSE 다이얼로그
END ON
END ON
// 영역 B: 회계코드 수정
ON 영역 B 행 [수정] CLICK DO
CALL acntgSeMngService.findById(acntgSeCd)
OPEN 회계코드 수정 다이얼로그 (기존 값 초기화)
ON [저장] DO
CALL acntgSeMngService.update(acntgSeCd, AcntgSeMngSaveRequestDto)
CALL acntgSeMngService.findAll
CLOSE 다이얼로그
END ON
END ON
// 영역 B: 회계코드 삭제
ON 영역 B 행 [삭제] CLICK DO
CONFIRM "선택한 회계코드를 삭제하시겠습니까?"
CALL acntgSeMngService.delete(acntgSeCd)
CALL acntgSeMngService.findAll
END ON
END
```
---
## 4. 영역 A: 공종코드 관리
백엔드 **공종구분코드**(`CnstpSeMng`) CRUD. 계층 구조(`upCnstpCd`, `cdLvNo`)를 지원하나, 본 화면은 **평면 목록**으로 표시.
### 4-1. 헤더 / 툴바
| 항목 | 내용 |
| ----------- | ------------------------------------- |
| 섹션 타이틀 | 공종코드 관리 |
| 우측 버튼 | **[등록]** — 공종코드 등록 다이얼로그 |
### 4-2. 그리드 컬럼
| 컬럼명 | Field | 타입 | 비고 |
| -------- | ------------- | ------ | ------------------------ |
| 코드 | `cnstpSeCd` | string | PK, 최대 10자 |
| 코드명 | `cnstpSeCdNm` | string | 최대 100자 |
| 설명 | `expln` | string | 최대 1000자, 선택 |
| 사용여부 | `useYn` | string | `Y`→사용, `N`→미사용 |
| 관리 | — | — | 행별 **[수정] · [삭제]** |
- 행 클릭 시 별도 상세 이동 없음. 수정은 **[수정]** 버튼으로 다이얼로그 진입.
- 페이지네이션: Spring `Pageable` (`page`, `size`, `sort`) — **UI 노출**. 기본 `size: 10`, `@common/components` 공통 페이지네이션 컴포넌트 사용.
### 4-3. 버튼
| 버튼명 | 위치 | 동작 |
| ------ | -------------- | ---------------------------------------- |
| 등록 | 섹션 우측 상단 | 공종코드 등록 다이얼로그 오픈 |
| 수정 | 행별 | 상세 조회 후 수정 다이얼로그 (값 초기화) |
| 삭제 | 행별 | 확인 후 삭제, 목록 갱신 |
### 4-4. 다이얼로그: 공종코드 등록 / 수정
다이얼로그 입력 필드는 **코드 · 코드명 · 설명 · 사용여부** 4개만 노출한다.
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
| -------- | --------- | ---- | ------ | ----------------- | ------------------------------- |
| 코드 | Input | Y | — | `cnstpSeCd` | 등록 시 입력, 수정 시 읽기 전용 |
| 코드명 | Input | N | — | `cnstpSeCdNm` | 최대 100자 |
| 설명 | Input | N | — | `expln` | 최대 1000자 |
| 사용여부 | Radio | N | Y | `useYn` | `Y`→사용, `N`→미사용 |
> UI에 노출하지 않는 API 필드(`upCnstpCd`, `cdLvNo`): 등록 시 `cdLvNo: 1` 등 기본값 전송, 수정 시 `findById` 응답값 유지.
| 버튼명 | 동작 |
| ------ | ------------------------------------------------ |
| 저장 | POST(등록) 또는 PUT(수정) 후 `findAll` 목록 갱신 |
| 취소 | 다이얼로그 닫기, 입력값 폐기 |
| 닫기 | 다이얼로그 닫기 |
---
## 5. 영역 B: 회계코드 관리
백엔드 **회계구분관리**(`AcntgSeMng`) CRUD. 영역 A와 동일한 **평면 목록** UI 패턴을 따른다.
### 5-1. 헤더 / 툴바
| 항목 | 내용 |
| ----------- | ------------------------------------- |
| 섹션 타이틀 | 회계코드 관리 |
| 우측 버튼 | **[등록]** — 회계코드 등록 다이얼로그 |
### 5-2. 그리드 컬럼
| 컬럼명 | Field | 타입 | 비고 |
| -------- | ------------- | ------ | ------------------------ |
| 코드 | `acntgSeCd` | string | PK, 최대 10자 |
| 코드명 | `acntgSeCdNm` | string | 최대 100자 |
| 설명 | `expln` | string | 최대 1000자, 선택 |
| 사용여부 | `useYn` | string | `Y`→사용, `N`→미사용 |
| 관리 | — | — | 행별 **[수정] · [삭제]** |
- 페이지네이션: 영역 A와 동일 — **UI 노출**, 기본 `size: 10`.
### 5-3. 버튼
| 버튼명 | 위치 | 동작 |
| ------ | -------------- | ---------------------------------------- |
| 등록 | 섹션 우측 상단 | 회계코드 등록 다이얼로그 오픈 |
| 수정 | 행별 | 상세 조회 후 수정 다이얼로그 (값 초기화) |
| 삭제 | 행별 | 확인 후 삭제, 목록 갱신 |
### 5-4. 다이얼로그: 회계코드 등록 / 수정
다이얼로그 입력 필드는 **코드 · 코드명 · 설명 · 사용여부** 4개만 노출한다. (영역 A와 동일 패턴)
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
| -------- | --------- | ---- | ------ | ----------------- | ------------------------------- |
| 코드 | Input | Y | — | `acntgSeCd` | 등록 시 입력, 수정 시 읽기 전용 |
| 코드명 | Input | Y | — | `acntgSeCdNm` | 최대 100자 |
| 설명 | Input | N | — | `expln` | 최대 1000자 |
| 사용여부 | Radio | N | Y | `useYn` | `Y`→사용, `N`→미사용 |
> UI에 노출하지 않는 API 필드(`upAcntgSeCd`, `cdLvNo`): 등록 시 `cdLvNo: 1` 등 기본값 전송, 수정 시 `findById` 응답값 유지.
| 버튼명 | 동작 |
| ------ | ------------------------------------------------ |
| 저장 | POST(등록) 또는 PUT(수정) 후 `findAll` 목록 갱신 |
| 취소 | 다이얼로그 닫기, 입력값 폐기 |
| 닫기 | 다이얼로그 닫기 |
---
## 6. API/DTO 명세
### 6-1. 공통
| 항목 | 내용 |
| ----------- | ---------------------------------------------------------------------------------- |
| 서비스 | `as-service` |
| Base prefix | `/as-service/api/v1` (`ENDPOINT.AS_COMMON_SERVICE`) |
| 인증 | JWT (`Authorization` 헤더), `X-Site-Id`, `token-id` |
| 페이징 | Spring `Pageable`: `page`, `size`, `sort` → `PageResponse<T>` |
| 엔티티 | `@/entities/cnstp` (`cnstpSeMngService`), `@/entities/acntg` (`acntgSeMngService`) |
**부가 API (본 화면 목록 외 참조용)**
| 리소스 | Method | URL | Response | 용도 |
| ------------ | ------ | --------------------- | --------------------- | --------------------------------- |
| 공종구분코드 | GET | `/cnstp-se/list/tree` | `CnstpSeMngTreeDto[]` | 계층 트리 (타 화면·상위코드 선택) |
| 회계구분관리 | GET | `/acntg-se/list/tree` | `AcntgSeMngTreeDto[]` | 계층 트리 (타 화면·상위코드 선택) |
### 6-2. 공종구분코드 (영역 A) — `cnstp-se`
| 기능 | Method | URL | Request / Query | Response | 클라이언트 |
| --------- | ------ | ----------------------- | -------------------------------- | ----------------------------- | ---------------------------- |
| 목록 조회 | GET | `/cnstp-se` | `CnstpSeMngSearchDto` + Pageable | `Page<CnstpSeMngResponseDto>` | `cnstpSeMngService.findAll` |
| 트리 조회 | GET | `/cnstp-se/list/tree` | — | `CnstpSeMngTreeDto[]` | `cnstpSeMngService.findTree` |
| 상세 조회 | GET | `/cnstp-se/{cnstpSeCd}` | — | `CnstpSeMngResponseDto` | `cnstpSeMngService.findById` |
| 등록 | POST | `/cnstp-se` | `CnstpSeMngSaveRequestDto` | `string` (cnstpSeCd) | `cnstpSeMngService.save` |
| 수정 | PUT | `/cnstp-se/{cnstpSeCd}` | `CnstpSeMngSaveRequestDto` | `string` (cnstpSeCd) | `cnstpSeMngService.update` |
| 삭제 | DELETE | `/cnstp-se/{cnstpSeCd}` | — | `string` (cnstpSeCd) | `cnstpSeMngService.delete` |
**CnstpSeMngResponseDto**
| Field | 타입 | 설명 |
| ------------- | ------- | ------------------ |
| `cnstpSeCd` | string | 공종구분코드 |
| `cnstpSeCdNm` | string | 공종구분코드명 |
| `upCnstpCd` | string? | 상위공종코드 |
| `cdLvNo` | number | 코드레벨번호 |
| `useYn` | string | 사용여부 |
| `expln` | string? | 설명 (최대 1000자) |
| `rgtr` | string? | 등록자 UUID |
| `rgtrNm` | string? | 등록자명 |
| `mdfr` | string? | 수정자 UUID |
| `regDt` | string? | 등록일시 |
| `mdfcnDt` | string? | 수정일시 |
**CnstpSeMngSaveRequestDto**: `cnstpSeCd`, `cnstpSeCdNm?`, `upCnstpCd?`, `cdLvNo?`, `useYn?`, `expln?`
**CnstpSeMngSearchDto**: `cnstpSeCd?`, `cnstpSeCdNm?`, `useYn?` + `page`, `size`, `sort`
**React Query hooks**: `useCnstpSeMngPage`, `useCnstpSeMngTree`, `useCnstpSeMng`, `useSaveCnstpSeMng`, `useUpdateCnstpSeMng`, `useDeleteCnstpSeMng`
### 6-3. 회계구분관리 (영역 B) — `acntg-se`
| 기능 | Method | URL | Request / Query | Response | 클라이언트 |
| --------- | ------ | ----------------------- | -------------------------------- | ----------------------------- | ---------------------------- |
| 목록 조회 | GET | `/acntg-se` | `AcntgSeMngSearchDto` + Pageable | `Page<AcntgSeMngResponseDto>` | `acntgSeMngService.findAll` |
| 트리 조회 | GET | `/acntg-se/list/tree` | — | `AcntgSeMngTreeDto[]` | `acntgSeMngService.findTree` |
| 상세 조회 | GET | `/acntg-se/{acntgSeCd}` | — | `AcntgSeMngResponseDto` | `acntgSeMngService.findById` |
| 등록 | POST | `/acntg-se` | `AcntgSeMngSaveRequestDto` | `string` (acntgSeCd) | `acntgSeMngService.save` |
| 수정 | PUT | `/acntg-se/{acntgSeCd}` | `AcntgSeMngSaveRequestDto` | `string` (acntgSeCd) | `acntgSeMngService.update` |
| 삭제 | DELETE | `/acntg-se/{acntgSeCd}` | — | `string` (acntgSeCd) | `acntgSeMngService.delete` |
**AcntgSeMngResponseDto**
| Field | 타입 | 설명 |
| ------------- | ------- | ------------------ |
| `acntgSeCd` | string | 회계구분코드 |
| `acntgSeCdNm` | string | 회계구분코드명 |
| `upAcntgSeCd` | string? | 상위회계구분코드 |
| `cdLvNo` | number | 코드레벨번호 |
| `useYn` | string | 사용여부 |
| `expln` | string? | 설명 (최대 1000자) |
| `rgtr` | string? | 등록자 UUID |
| `rgtrNm` | string? | 등록자명 |
| `mdfr` | string? | 수정자 UUID |
| `regDt` | string? | 등록일시 |
| `mdfcnDt` | string? | 수정일시 |
**AcntgSeMngSaveRequestDto**: `acntgSeCd`, `acntgSeCdNm`, `upAcntgSeCd?`, `cdLvNo`, `useYn?`, `expln?`
**AcntgSeMngSearchDto**: `acntgSeCd?`, `acntgSeCdNm?`, `useYn?` + `page`, `size`, `sort`
**React Query hooks**: `useAcntgSeMngPage`, `useAcntgSeMngTree`, `useAcntgSeMng`, `useSaveAcntgSeMng`, `useUpdateAcntgSeMng`, `useDeleteAcntgSeMng`
---
## 7. UI 구현 가이드
### 7-1. 기술 스택·원칙
- Next.js App Router, React Query (`@tanstack/react-query`)
- `@common/components` — `Card`, `Button`, `Dialog`, `DataTable` / `useDataTable`
- `@tanstack/react-table` — 컬럼 정의는 `model/*-table-columns.tsx` 분리
- 기존 entity 레이어 재사용: `@/entities/cnstp`, `@/entities/acntg` (service·queries·interface 구현 완료)
- 좌·우 2열 레이아웃: `grid grid-cols-1 lg:grid-cols-2 gap-4` (모바일 1열 스택)
- 삭제 전 `confirm` / Toast 피드백 — 프로젝트 공통 패턴 준수
- 목록 하단 **페이지네이션 UI 노출** — 영역 A·B 각각 독립 page state
- **검색 필터 UI 미제공** — `findAll` 호출 시 `page`, `size`만 전달
- **등록/수정 다이얼로그** — 코드·코드명·설명은 `Input`, 사용여부는 `Radio` (4필드만 노출)
- 상위 코드 삭제 시 **하위 코드 연쇄 삭제 UI·로직 미구현** — 단건 삭제만 수행
### 7-2. 모듈·파일 배치
| 구분 | 경로 (제안) |
| ----------------- | -------------------------------------------------------------------- |
| 라우트 | `apps/as/src/app/(authenticated)/site/code-management/page.tsx` |
| 페이지 컴포넌트 | `apps/as/src/pages/code-management/ui/code-management-page.tsx` |
| 공종 그리드 컬럼 | `apps/as/src/pages/code-management/model/cnstp-se-table-columns.tsx` |
| 회계 그리드 컬럼 | `apps/as/src/pages/code-management/model/acntg-se-table-columns.tsx` |
| 공종 등록/수정 폼 | `apps/as/src/pages/code-management/ui/cnstp-se-form-dialog.tsx` |
| 회계 등록/수정 폼 | `apps/as/src/pages/code-management/ui/acntg-se-form-dialog.tsx` |
| API·타입 (기존) | `@/entities/cnstp`, `@/entities/acntg` |
### 7-3. 영역별 UI 매핑
| 영역 | UI 컴포넌트 | 데이터 소스 | 비고 |
| ------------ | ------------------------------------- | ------------------------------------------- | ------------------------------ |
| A | `CnstpSeSection` + Table + Pagination | `useCnstpSeMngPage({ page, size })` | 독립 page state·mutation |
| B | `AcntgSeSection` + Table + Pagination | `useAcntgSeMngPage({ page, size })` | A와 상호 의존 없음 |
| A 다이얼로그 | `CnstpSeFormDialog` | `useCnstpSeMng`, `useSave/UpdateCnstpSeMng` | Input 4필드 + Radio 사용여부 |
| B 다이얼로그 | `AcntgSeFormDialog` | `useAcntgSeMng`, `useSave/UpdateAcntgSeMng` | Input 4필드 + Radio 사용여부 |
### 7-4. 다이얼로그·라우팅
| 다이얼로그 | 트리거 | 라우팅 |
| ------------- | ---------------- | -------------------- |
| 공종코드 등록 | 영역 A [등록] | URL 변경 없음 (모달) |
| 공종코드 수정 | 영역 A 행 [수정] | URL 변경 없음 (모달) |
| 회계코드 등록 | 영역 B [등록] | URL 변경 없음 (모달) |
| 회계코드 수정 | 영역 B 행 [수정] | URL 변경 없음 (모달) |
### 7-5. 데이터 흐름 (구현 관점)
```
BEGIN
// 페이지 마운트 — 검색 조건 없이 page·size만 전달
const cnstpQuery = useCnstpSeMngPage({ page: 0, size: 20 })
const acntgQuery = useAcntgSeMngPage({ page: 0, size: 20 })
DISPLAY CodeManagementPage
├─ CnstpSeSection(data=cnstpQuery.data, pagination)
└─ AcntgSeSection(data=acntgQuery.data, pagination)
// 페이지 변경
ON pagination page/size CHANGE DO
refetch cnstpQuery | acntgQuery with new page, size
END ON
// mutation 성공 시
ON save/update/delete SUCCESS DO
invalidateQueries cnstp-se | acntg-se baseKey
TOAST "저장되었습니다." | "삭제되었습니다."
END ON
END
```
@@ -0,0 +1,284 @@
# 화면설계서: 필수첨부파일관리
## 1. 기본 정보
| 항목 | 내용 |
| ---------- | ---------------------------------------------- |
| 버전 | 0.1 |
| 프로세스명 | UI설계 |
| 화면명 | 필수첨부파일관리 |
| 화면 유형 | [v] 조회 [v] 입력 [v] 수정 [v] 삭제 [ ] 출력 |
| 메뉴 경로 | 디지털행정지원 > 사이트관리 > 필수첨부파일관리 |
---
## 2. 화면 목적
문서(업무) 구분별로 **필수 첨부파일** 항목을 등록·조회·수정·삭제한다. 상단 검색 영역에서 **문서구분**으로 목록을 필터링하고, **등록** 또는 행별 **수정**·**삭제**로 마스터 데이터를 유지한다. 등록·수정 시 **필수첨부파일아이디**는 클라이언트에서 입력(또는 생성)하여 요청 본문에 포함한다.
---
## 3. 화면 구성
### 3-1. 전체 레이아웃
```
┌──────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 페이지 타이틀: 필수첨부파일관리 브레드크럼: 디지털행정지원 > … > │
├──────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ [영역 A] 검색 / 액션 │
│ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ 문서구분 [전체 ▼] [조회] [등록] │ │
│ └────────────────────────────────────────────────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ [영역 B] 필수첨부파일 목록 │
│ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ 필수첨부파일아이디 | 문서구분 | 필수첨부파일명 | 필수첨부여부 | 비고 | 등록일시 | 수정일시 | 관리 │
│ ├────────────────────────────────────────────────────────────────────────────────────────────────┤ │
│ │ EAF-0001 | 설치인가 | 사업계획서 | 예 | 서명본 첨부 | 2026-04-22 10:10:00 | — | [수정][삭제] │ │
│ │ EAF-0002 | 기본계획 | 변경사유서 | 예 | — | 2026-04-22 11:00:00 | — | [수정][삭제] │ │
│ └────────────────────────────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────┘
(모달) [다이얼로그 C] 필수첨부파일 등록 / 수정
┌──────────────────────────────────────────────────────┐
│ 필수첨부파일 등록 [닫기] │
├──────────────────────────────────────────────────────┤
│ 필수첨부파일아이디(자동발급) [EAF-0004] │
│ 문서구분 [설치인가 ▼] │
│ 필수첨부파일명 [예: 사업계획서] │
│ 필수첨부여부 [ ] 예 [ ] 아니오 │
│ 비고 [비고를 입력하세요] │
├──────────────────────────────────────────────────────┤
│ [취소] [저장] │
└──────────────────────────────────────────────────────┘
```
### 3-2. 화면 데이터 흐름
```
BEGIN
// 최초 진입
CALL 필수첨부파일 목록 조회 (findPage)
DISPLAY 필수첨부파일 그리드 (영역 B)
// 영역 A: 검색
ON [조회] 버튼 CLICK DO
CALL 필수첨부파일 목록 조회 (문서구분 = 선택값, findPage)
DISPLAY 필수첨부파일 그리드 (영역 B)
END ON
// 영역 A: 등록
ON [등록] 버튼 CLICK DO
OPEN 필수첨부파일 등록 다이얼로그 (다이얼로그 C)
ON [저장] DO
CONFIRM "등록하시겠습니까?"
VALIDATE 문서구분, 필수첨부파일명, 필수첨부여부
CALL 필수첨부파일 등록 (create)
CALL 필수첨부파일 목록 조회
CLOSE 다이얼로그
TOAST "저장되었습니다."
END ON
ON [취소] / [닫기] DO
CLOSE 다이얼로그
END ON
END ON
// 영역 B: 수정
ON 행 [수정] CLICK DO
OPEN 필수첨부파일 수정 다이얼로그 (기존 값 초기화)
ON [저장] DO
VALIDATE 문서구분, 필수첨부파일명, 필수첨부여부
CALL 필수첨부파일 수정 (update)
CALL 필수첨부파일 목록 조회
CLOSE 다이얼로그
END ON
END ON
// 영역 B: 삭제
ON 행 [삭제] CLICK DO
CONFIRM "삭제하시겠습니까?"
CALL 필수첨부파일 삭제 (remove)
CALL 필수첨부파일 목록 조회
END ON
END
```
---
## 4. 영역 A: 검색 / 액션
문서구분 조건으로 목록을 조회하고, 신규 등록 다이얼로그를 연다.
### 4-1. 검색 필드
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
| -------- | ---------- | ---- | ------ | ----------------- | ------------------------------ |
| 문서구분 | CodeSelect | N | 전체 | `docSeComCd?` | "전체" 선택 시 파라미터 미전송 |
### 4-2. 버튼
| 버튼명 | 위치 | 동작 |
| ------ | -------------- | ----------------------------------------------- |
| 조회 | 검색 필드 우측 | 현재 검색 조건으로 영역 B 목록 갱신 |
| 등록 | 영역 우측 끝 | 필수첨부파일 등록 다이얼로그(다이얼로그 C) 오픈 |
---
## 5. 영역 B: 필수첨부파일 목록
필수 첨부파일 마스터를 테이블로 표시한다. 페이지네이션 UI 적용.
### 5-1. 헤더
| 항목 | 내용 |
| ----------- | ----------------- |
| 섹션 타이틀 | 필수첨부파일 목록 |
### 5-2. 그리드 컬럼
| 컬럼명 | Field | 타입 | 비고 |
| ------------------ | ------------------- | ------ | ------------------------------------- |
| 필수첨부파일아이디 | `esntlAtchFileId` | string | 예: `EAF-0001`, 클라이언트 생성/입력 |
| 문서구분 | `docSeComCd` | string | 코드명 표시 — 공통코드 (TBD 상위코드) |
| 필수첨부파일명 | `esntlAtchFileNm` | string | |
| 필수첨부여부 | `esntlAtchYn` | string | `Y`→예, `N`→아니오 |
| 비고 | `esntlAtchFileRmrk` | string | 없으면 `—` 표시 |
| 등록일시 | `regDt` | string | `yyyy-MM-dd HH:mm:ss` |
| 수정일시 | `mdfcnDt` | string | 미수정 시 빈 값 또는 `—` |
| 관리 | — | — | 행별 **[수정] · [삭제]** |
- 행 클릭 시 별도 상세 화면 없음. 편집은 **[수정]** 버튼으로 다이얼로그 C를 연다.
### 5-3. 버튼
| 버튼명 | 위치 | 동작 |
| ------ | ---- | -------------------------------------- |
| 수정 | 행별 | 필수첨부파일 수정 다이얼로그 (기존 값) |
| 삭제 | 행별 | 확인 후 삭제 API 호출 → 목록 갱신 |
---
## 6. 다이얼로그 C: 필수첨부파일 등록 / 수정
등록·수정 공용 폼 다이얼로그. 등록 시 제목 **「필수첨부파일 등록」**, 수정 시 **「필수첨부파일 수정」**.
### 6-1. 폼 필드
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
| ------------------ | ---------- | ---- | ------ | ------------------- | -------------------------------- |
| 필수첨부파일아이디 | (Readonly) | Y | — | `esntlAtchFileId` | 등록 시 미노출 |
| 문서구분 | CodeSelect | Y | — | `docSeComCd` | 예: 설치인가, 기본계획 |
| 필수첨부파일명 | Text | Y | — | `esntlAtchFileNm` | placeholder: "예: 사업계획서" |
| 필수첨부여부 | RadioGroup | Y | Y | `esntlAtchYn` | Y/N, UI 표시: 예/아니오 |
| 비고 | Textarea | N | — | `esntlAtchFileRmrk` | placeholder: "비고를 입력하세요" |
### 6-2. 버튼
| 버튼명 | 동작 |
| ------ | -------------------------------------- |
| 저장 | POST(등록) 또는 PUT(수정) 후 목록 갱신 |
| 취소 | 다이얼로그 닫기, 입력값 폐기 |
| 닫기 | 다이얼로그 닫기 (헤더 우측) |
---
## 7. API/DTO 명세
### 7-1. 공통
| 항목 | 내용 |
| ----------- | ----------------------------------------------------- |
| 서비스 | AS Common Service |
| Base prefix | `/as-service/api/v1/esntl-atch-file` |
| 인증 | JWT (`Authorization`), `X-Site-Id` 등 |
| 페이징 | Spring `Pageable` 사용 (`page`, `size`, `sort`) |
| 엔티티 | `@/entities/esntl` (`esntl-service.ts`, `queries.ts`) |
### 7-2. 필수첨부파일 API
| 기능 | Method | URL | Request DTO | Response DTO | API (클라이언트 함수명) |
| --------- | ------ | -------------------- | ----------------------------- | ----------------------------------- | ----------------------- |
| 트리 조회 | GET | `/list/tree` | Query: `docSeComCd`(필수) | `EsntlAtchFileMngTreeResponseDto[]` | `findTree` |
| 목록 조회 | GET | `/` | Query: Search + Pageable | `PageResponse<EsntlAtchFileMng>` | `findPage` |
| 단건 조회 | GET | `/{esntlAtchFileId}` | Path: `esntlAtchFileId` | `EsntlAtchFileMng` | `findById` |
| 등록 | POST | `/` | `EsntlAtchFileMngSaveRequest` | `string` (생성된 ID) | `create` |
| 수정 | PUT | `/{esntlAtchFileId}` | `EsntlAtchFileMngSaveRequest` | `string` (수정된 ID) | `update` |
| 삭제 | DELETE | `/{esntlAtchFileId}` | Path: `esntlAtchFileId` | `string` (삭제된 ID) | `remove` |
**응답 DTO**
| Field | 타입 | 설명 |
| ------------------- | ------- | -------------------------------- |
| `esntlAtchFileId` | string | 필수첨부파일아이디 |
| `docSeComCd` | string | 문서구분공통코드 |
| `esntlAtchFileNm` | string | 필수첨부파일명 |
| `esntlAtchYn` | string | 필수첨부여부 (Y/N) |
| `esntlAtchFileRmrk` | string? | 필수첨부파일비고 |
| `rgtr` | string? | 등록자 UUID |
| `rgtrNm` | string? | 등록자명 |
| `mdfr` | string? | 수정자 UUID |
| `regDt` | string? | 등록일시 (`yyyy-MM-dd HH:mm:ss`) |
| `mdfcnDt` | string? | 수정일시 |
**저장 Request**
| Field | 필수 | 설명 |
| ------------------- | ---- | ------------------ |
| `esntlAtchFileId` | Y | 필수첨부파일아이디 |
| `docSeComCd` | N | 문서구분 |
| `esntlAtchFileNm` | N | 필수첨부파일명 |
| `esntlAtchYn` | N | 필수첨부여부 |
| `esntlAtchFileRmrk` | N | 비고 |
**검색 Query**
| Field | 필수 | 설명 |
| ----------------- | ---- | ----------------------------- |
| `docSeComCd` | N | 문서구분 (전체 시 미전송) |
| `esntlAtchFileNm` | N | 필수첨부파일명 부분/일치 검색 |
| `esntlAtchYn` | N | 필수첨부여부 (`Y`/`N`) |
| `page` | N | 페이지 번호 (0-base) |
| `size` | N | 페이지 크기 (기본값: 100) |
| `sort` | N | 정렬 조건 (예: `regDt,desc`) |
### 7-3. 공통코드
| UI 필드 | 코드 그룹 | 예시 값 |
| ------------ | --------- | ------------------ |
| 문서구분 | ASDOCSECD | 설치인가, 기본계획 |
| 필수첨부여부 | 고정 | Y(예), N(아니오) |
---
## 8. UI 구현 가이드
### 8-1. 기술 스택·원칙
- **공통 컴포넌트**: `@common/components` (`Dialog`, `Button`, `Input`, `Select`, `Textarea`, `Label` 등)
- **테이블**: `@tanstack/react-table` + `useDataTable`
- **폼**: `react-hook-form`
- **데이터**: React Query — `@/entities/esntl/api/queries.ts` 훅 사용
- **공통코드**: `@/entities/code` — `CodeSelect`, `findCodeLabel`
- **확인**: `@frontend-opensource/use-react-hooks` — `useConfirm` (등록·삭제 확인)
### 8-2. 모듈·파일 배치
| 구분 | 경로 |
| ------ | ---------------------------------------------------------------------------------------------------- |
| 라우트 | `apps/as/app/(with-base-layout)/esntl-atch-file-mng/page.tsx` |
| 페이지 | `apps/as/src/pages/esntl-atch-file-mng/` (Public API: page만 export) |
| UI | `.../ui/esntl-atch-file-mng-page.tsx`, `esntl-atch-file-list.tsx`, `esntl-atch-file-form-dialog.tsx` |
| model | `.../model/esntl-atch-file-columns.tsx`, `esntl-atch-file-search-params.ts` |
| 엔티티 | `apps/as/src/entities/esntl/` (`esntl-service.ts`, `queries.ts`, `esntl-interface.ts`) |
| 문서 | `apps/as/docs/essential-attachment-file-management/SPEC.md` |
### 8-3. 데이터 흐름(요약)
- **진입**: 검색 params 기본값(`docSeComCd` = 전체) → 목록 query → 영역 B DataTable.
- **조회**: [조회] 클릭 시 search state 반영 → 목록 query refetch.
- **등록**: [등록] → Dialog `mode=create` → [저장] → POST → 목록 invalidate.
- **수정**: [수정] → Dialog `mode=edit` + row 초기값 → [저장] → PUT → 목록 invalidate.
- **삭제**: confirm → DELETE → 목록 invalidate.
@@ -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` |
@@ -0,0 +1,370 @@
# 화면설계서: 검토인력관리
## 1. 기본 정보
| 항목 | 내용 |
| ---------- | -------------------------------------------- |
| 버전 | 0.2 |
| 프로세스명 | UI설계 |
| 화면명 | 검토인력관리 |
| 화면 유형 | [v] 조회 [v] 입력 [ ] 수정 [v] 삭제 [ ] 출력 |
| 메뉴 경로 | 디지털행정지원 > 사이트관리 > 검토인력관리 |
---
## 2. 화면 목적
조직도에서 **한 명의 사용자**를 선택한 뒤, 결재 단계(접수·검토·승인·최종승인)별 **인원 배치** 영역에 즉시 등록한다. 등록·목록 갱신은 별도 저장 버튼 없이 **[선택 인원 배치]** 클릭 시 `rvw-mnpw` API로 바로 반영한다. 좌측 조직도는 CMS 조직·사용자 트리 API를, 우측 배치 목록은 검토인력관리 API를 사용한다.
---
## 3. 화면 구성
### 3-1. 전체 레이아웃
```
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 페이지 타이틀: 검토인력관리 브레드크럼: 디지털행정지원 > 사이트관리 > 검토인력관리 │
├─────────────────────────────────┬──────────────────────────────────────────────────────────────────┤
│ [영역 A] 조직도 │ [영역 B] 인원 배치 (결재구분별 4블록, 세로 스택) │
│ 제목: 조직도 │ │
│ [A-1] 검색: │ ┌─ [B-1] 접수 ───────────── count ─ [선택 인원 배치] ─┐ │
│ [성명·부서 검색] [조회][초기화]│ │ (배치 목록 또는 빈 상태 문구) │ │
│ 선택 안내: │ └──────────────────────────────────────────────────────┘ │
│ "선택된 인원: {이름} │ ┌─ [B-2] 검토 ───────────── count ─ [선택 인원 배치] ──┐ │
│ ({부서} / {직급})" │ └──────────────────────────────────────────────────────┘ │
│ [A-2] 조직 트리 + 사용자 행 │ ┌─ [B-3] 승인 ───────────── count ─ [선택 인원 배치] ──┐ │
│ ┌────────────────────┐ │ └──────────────────────────────────────────────────────┘ │
│ │ 조직 트리 + 사용자 │ │ ┌─ [B-4] 최종승인 ───────── count ─ [선택 인원 배치] ──┐ │
│ │ (단일 선택 강조) │ │ └──────────────────────────────────────────────────────┘ │
│ └────────────────────┘ │ │
└─────────────────────────────────┴──────────────────────────────────────────────────────────────────┘
```
### 3-2. 화면 데이터 흐름
```
BEGIN
// 최초 진입
CALL userOgnzService.findOgnzUserTree()
DISPLAY 조직도 트리 (영역 A)
CALL rvwMnpwMngService.findPage({ size: 충분히 큰 값 }) // 또는 atrzSeComCd별 분할 조회
GROUP BY atrzSeComCd
DISPLAY 인원 배치 4블록 (영역 B-1 ~ B-4) + 건수 뱃지
// 영역 A-1: 조직도 검색 (클라이언트 필터 — 서버 API 없음)
ON [조회] 버튼 CLICK / 검색 폼 SUBMIT DO
SET displayTree = filterOgnzUserTree(원본트리, keyword)
SET expandedOgnzSnIds = collectExpandedOgnzSnIds(displayTree) // 매칭 노드 자동 펼침
DISPLAY 필터된 트리 (영역 A-2)
END ON
ON [초기화] 버튼 CLICK DO
RESET keyword = ""
RESET displayTree = 원본트리
RESET expandedOgnzSnIds = 빈 Set (또는 기본 접힘)
DISPLAY 전체 트리 (영역 A-2)
END ON
// 영역 A-2: 사용자 단일 선택
ON 조직도 사용자 행 CLICK DO
VALIDATE 동일 행 재클릭이 아닌 경우만 변경
SET selectedPerson = { userUuid, userNm, ognzSn, ognzNm, jbgdSeCd, ... }
DISPLAY "선택된 인원: {userNm} ({ognzNm} / {직급명})"
END ON
// 영역 B-k: 선택 인원 즉시 배치
ON [B-k] [선택 인원 배치] 버튼 CLICK DO
VALIDATE selectedPerson 존재
SET targetAtrzSeComCd = 블록 k 에 매핑된 결재구분 코드 (§4-2 표)
CALL rvwMnpwMngService.save({
atrzSeComCd: targetAtrzSeComCd,
ognzSn: selectedPerson.ognzSn,
userUuid: selectedPerson.userUuid
})
ON 성공 DO
CALL rvwMnpwMngService.findPage (또는 해당 블록 목록만 갱신)
DISPLAY [B-k] 배치 목록·건수 뱃지 갱신
TOAST "배치되었습니다."
END ON
ON 실패(이미 존재) DO
TOAST (백엔드 메시지 연동)
END ON
END ON
// 영역 B-k: 배치 인원 제거 (UI 상 삭제 UX — 스크린샷 미표기, API는 제공됨)
ON 배치 인원 [삭제] / [제거] CLICK DO
CONFIRM "해당 인원을 배치에서 제거하시겠습니까?"
CALL rvwMnpwMngService.delete(atrzSeComCd, ognzSn, userUuid)
CALL rvwMnpwMngService.findPage
DISPLAY [B-k] 목록·뱃지 갱신
END ON
END
```
---
## 4. 영역 A — 조직도
### 4-1. 표시·상호작용
| 요소 | 내용 |
| ---------------- | --------------------------------------------------------------------- |
| 섹션 제목 | 조직도 |
| 검색 폼 (A-1) | 성명·부서 키워드 입력 + **[조회]** + **[초기화]** — §4-3 |
| 선택 안내 문구 | `선택된 인원: {userNm} ({ognzNm} / {직급표시})` — 미선택 시 안내 문구 |
| 트리 (A-2) | 본부 → 팀 계층 + 팀 소속 사용자 목록 (`displayTree` 기준 표시) |
| 선택 규칙 | **사용자 1명만** 선택 가능 (라디오형 단일 선택) |
| 선택 시각 피드백 | 선택된 사용자 행 강조(배경·테두리) |
| 로딩/오류/빈값 | Skeleton / 오류 문구 / "등록된 조직/사용자가 없습니다." |
| 검색 결과 없음 | 필터 후 매칭 노드·사용자 0건. "조회된 인원이 없습니다." 문구 노출 |
### 4-2. 조직도 데이터 매핑
| UI 표시 | 트리 DTO 필드 (`OgnzUserTreeResponseDto`) | 비고 |
| --------------- | ----------------------------------------- | ----------------------------------------- |
| 조직명 | `ognzNm` | 접기/펼치기 노드 |
| 사용자명 | `users[].userNm` | 클릭 시 선택 |
| 부서(안내 문구) | `ognzNm` (소속 팀 노드) | 선택 시 `selectedPerson.ognzNm` |
| 직급(안내 문구) | `users[].jbgdSeCd` 또는 `jbpsSeCd` | 공통코드 라벨 변환 |
| 저장용 조직 SN | `ognzSn` | 사용자가 속한 **리프/팀 노드**의 `ognzSn` |
| 저장용 사용자 | `users[].userUnqId` | API body `userUuid`로 전달 (§6-3) |
### 4-3. 조직도 검색 (영역 A-1)
결재선 관리 `AprvrPickerDialog`와 동일한 **클라이언트 사이드 필터** 패턴을 사용한다. 별도 검색 API는 없으며, `useOgnzUserTree`로 받은 **원본 트리**를 `filterOgnzUserTree`로 가공해 `displayTree`에 반영한다.
#### 필드 정의
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
| -------------- | --------- | ---- | ------ | ----------------- | ----------------------------------------- |
| 성명·부서 검색 | Text | N | (빈값) | — (클라이언트만) | `placeholder="성명·부서 검색"` |
| 조회 | Button | — | — | — | `type="submit"` — 폼 제출 시 필터 적용 |
| 초기화 | Button | — | — | — | `type="button"` — 키워드·트리·펼침 초기화 |
#### 필터 규칙 (`filterOgnzUserTree`)
| 대상 | 매칭 조건 |
| ---------------- | ----------------------------------------------------- |
| 조직명 | `ognzNm`에 키워드 포함 (대소문자 무시) |
| 사용자명 | `userNm`에 키워드 포함 (대소문자 무시) |
| 조직 매칭 시 | 해당 노드 **소속 사용자 전원** 유지 |
| 사용자만 매칭 시 | 매칭 사용자만 남기고 상위 조직 노드는 유지(경로 보존) |
| 하위 조직 | 재귀 필터 — 자식·손자 노드 매칭 시 상위도 표시 |
| 키워드 빈값 | 원본 트리 전체 반환 |
#### 조회 후 트리 펼침 (`collectExpandedOgnzSnIds`)
- **[조회]** 성공 후 필터 결과 트리의 **모든 노드** `ognzSn`을 `expandedOgnzSnIds`에 넣어 자동 펼침
- 검색 결과에서 사용자·하위 조직을 바로 확인할 수 있도록 함
#### 초기화 동작
| 상태 | 초기화 후 값 |
| ------------------- | --------------------------------------- |
| `keyword` | `""` |
| `displayTree` | `useOgnzUserTree` 원본 `data` |
| `expandedOgnzSnIds` | `new Set()` (전체 접힘) |
| `selectedPerson` | **유지** — 검색 초기화와 선택 해제 분리 |
#### 버튼
| 버튼명 | 동작 |
| ------ | --------------------------------------------------------- |
| 조회 | `filterOgnzUserTree(data, keyword)` → `displayTree` 갱신 |
| 초기화 | 키워드·표시 트리·펼침 상태 리셋 (§3-2 `ON [초기화]` 블록) |
---
## 5. 영역 B — 인원 배치
### 5-1. 블록 공통
| 요소 | 내용 |
| ------------------ | ------------------------------------------------- |
| 블록 제목 | 접수 / 검토 / 승인 / 최종승인 |
| 건수 뱃지 | 해당 `atrzSeComCd`에 배치된 인원 수 |
| 액션 버튼 | **[선택 인원 배치]** — 클릭 시 즉시 `save` (§3-2) |
| 빈 상태 | "배치된 인원이 없습니다." |
| 배치 목록(있을 때) | 인원명·부서·직급 등 |
### 5-2. 결재구분 ↔ 블록 매핑
| 블록 | UI 제목 | `atrzSeComCd` | 비고 |
| ---- | -------- | ------------- | -------------------------------------------------------------- |
| B-1 | 접수 | `ATRZ00` | 결재선 관리 `ATRZ_RECEIVER_CD`와 동일 |
| B-2 | 검토 | `ATRZ01` | 결재선 `ATRZ_REVIEWER_1_CD`. `ATRZ02` 별도 블록 여부 — **TBD** |
| B-3 | 승인 | `ATRZ03` | 결재선 `ATRZ_APPROVER_CD` |
| B-4 | 최종승인 | `ATRZ99` | 결재선 `ATRZ_FINAL_CD` |
공통코드 마스터 `ATRZSE` / 화면 셀렉트용 `ATRZSECD`와 정합성은 배포 환경 코드표로 최종 확인.
### 5-3. 배치 목록 항목 (표시)
| 표시명 | Field (응답 DTO) | 비고 |
| -------- | ---------------- | ---------------------------- |
| 사용자명 | `userUuidNm` | `@CodeName` 미적용 시 TBD |
| 결재구분 | `atrzSeComCdNm` | 블록 제목과 중복 — 생략 가능 |
| 조직 SN | `ognzSn` | 삭제 API path에 사용 |
| 사용자 | `userUuid` | 삭제 API path에 사용 |
### 5-4. 버튼
| 버튼명 | 위치 | 동작 |
| -------------- | ----------- | --------------------------------- |
| 선택 인원 배치 | 각 B-k 헤더 | `save` 후 목록·뱃지 갱신 (§3-2) |
| 제거 | 목록 항목 | `delete` 후 목록·뱃지 갱신 (§3-2) |
### 5-5. 검증·제약
| 규칙 | 내용 |
| ------------- | ------------------------------------------------------------------- |
| 선택 필수 | `[선택 인원 배치]` 시 `selectedPerson` 없으면 버튼 비활성 또는 안내 |
| 단일 선택 | 조직도에서 사용자 1명만 `selectedPerson` 유지 |
| 중복 등록 | 동일 PK `(atrzSeComCd, ognzSn, userUuid)` 재등록 시 서버 오류 |
| 다건 배치 | 동일 블록에 서로 다른 사용자·조직은 **복수 행** 가능 (PK가 다름) |
| 결재구분 변경 | 본 화면 주 흐름은 `save`만 |
---
## 6. API/DTO 명세
### 6-1. 공통
| 항목 | 내용 |
| ------------- | ------------------------------------------------------------- |
| 인증 | JWT (`Authorization` 헤더) |
| 페이징 | Spring `Pageable`: `page`, `size`, `sort` → `PageResponse<T>` |
| 프론트 엔티티 | `@/entities/user-organization`, `@/entities/rvw-mnpw` |
### 6-2. 조직·사용자 트리 (영역 A) — `user-ognz`
| 항목 | 내용 |
| ----------- | ------------------------------- |
| 서비스 | `cms-service` |
| Base prefix | `/cms-service/api/v1/user-ognz` |
| 기능 | Method | URL | Request DTO | Response DTO | API |
| ---------------- | ------ | ----------- | ----------- | --------------------------- | ---------------------------------- |
| 조직·사용자 트리 | GET | `/treeuser` | — | `OgnzUserTreeResponseDto[]` | `userOgnzService.findOgnzUserTree` |
**`OgnzUserTreeResponseDto`**: `ognzSn`, `upOgnzSn`, `ognzNm`, `users[]`, `children[]`
**`OgnzUserTreeUserDto`**: `userUnqId`, `userNm`, `jbpsSeCd`, `jbgdSeCd`
**React Query**: `useOgnzUserTree`
### 6-3. 검토인력관리 (영역 B) — `rvw-mnpw`
| 항목 | 내용 |
| ----------- | ------------------------------------- |
| 서비스 | `as-service` |
| Base prefix | `/as-service/api/v1/rvw-mnpw` |
| ENDPOINT | `ENDPOINT.AS_COMMON_SERVICE.RVW_MNPW` |
| 기능 | Method | URL | Request DTO / Query | Response DTO | API |
| --------------- | ------ | ------------------------------------ | -------------------------------- | ----------------------------- | ---------------------------- |
| 목록 조회 | GET | `/` | `RvwMnpwMngSearchDto` + Pageable | `Page<RvwMnpwMngResponseDto>` | `rvwMnpwMngService.findPage` |
| 상세 조회 | GET | `/{atrzSeComCd}/{ognzSn}/{userUuid}` | Path | `RvwMnpwMngResponseDto` | `rvwMnpwMngService.findById` |
| 등록(즉시 배치) | POST | `/` | `RvwMnpwMngSaveRequestDto` | `RvwMnpwMngResponseDto` (201) | `rvwMnpwMngService.save` |
| 결재구분 변경 | PUT | `/{atrzSeComCd}/{ognzSn}/{userUuid}` | `RvwMnpwMngUpdateRequestDto` | `RvwMnpwMngResponseDto` | `rvwMnpwMngService.update` |
| 삭제 | DELETE | `/{atrzSeComCd}/{ognzSn}/{userUuid}` | Path | `string` | `rvwMnpwMngService.delete` |
**`RvwMnpwMngSearchDto`**: `atrzSeComCd?`, `ognzSn?`, `userUuid?` + `page`, `size`, `sort`
**`RvwMnpwMngSaveRequestDto`**: `atrzSeComCd*`, `ognzSn*`, `userUuid*`
**`RvwMnpwMngUpdateRequestDto`**: `newAtrzSeComCd*` — 본 화면 기본 흐름에서는 미사용
**`RvwMnpwMngResponseDto`**: `atrzSeComCd`, `atrzSeComCdNm?`, `ognzSn`, `userUuid`, `userUuidNm?`, `rgtr?`, `regDt?`
### 6-4. 조직도 ↔ 검토인력 필드 매핑 (저장 시)
| 조직도 (`OgnzUserTreeUserDto` + 노드) | `RvwMnpwMngSaveRequestDto` | 비고 |
| ------------------------------------- | -------------------------- | --------------------- |
| (블록 k의 `atrzSeComCd`) | `atrzSeComCd` | §5-2 표 |
| 소속 팀 `ognzSn` | `ognzSn` | |
| `userUnqId` | `userUuid` | 문자열(UUID)로 직렬화 |
---
## 7. UI 구현 가이드
### 7-1. 기술 스택·원칙
- **공통 컴포넌트**: `@common/components` (`Button`, `Badge`, `Card` 또는 섹션 레이아웃)
- **데이터**: React Query — `useOgnzUserTree`, `useRvwMnpwMngPage`, `useSaveRvwMnpwMng`, `useDeleteRvwMnpwMng`
- **상태**: `selectedPerson`(단일), `keyword`, `displayTree`, `expandedOgnzSnIds`, 블록별 배치 목록(서버 데이터 파생)
- **조직도 검색**: `filterOgnzUserTree`, `collectExpandedOgnzSnIds` (`widgets/ognz-user-tree/lib` 또는 페이지 `lib`)
- **저장 UX**: 별도 화면 저장 없음 — **[선택 인원 배치]** = `save` mutation 즉시 호출
- **조직도 UI**: 결재선 관리 `ognz-user-tree.tsx`를 **widgets**로 승격해 공통화
### 7-2. 모듈·파일 배치(제안)
| 구분 | 경로 |
| -------- | ---------------------------------------------------------------------------------- |
| 라우트 | `apps/as/app/(with-base-layout)/rvw-mnpw-mng/page.tsx` |
| 페이지 | `apps/as/src/pages/rvw-mnpw-mng/` |
| UI | `.../ui/rvw-mnpw-mng-page.tsx`, `rvw-mnpw-assignment-panel.tsx` 등 |
| model | `.../model/selected-ognz-user.ts`, `rvw-mnpw-block-config.ts` (`atrzSeComCd` 상수) |
| lib | `.../lib/group-rvw-mnpw-by-atrz-se.ts` (목록 → B-1~B-4 분배) |
| **위젯** | `apps/as/src/widgets/ognz-user-tree/` — 아래 이전 대상 |
| 엔티티 | `entities/user-organization/`, `entities/rvw-mnpw/` (구현 완료) |
#### widgets/ognz-user-tree 이전 (결재선 → 공통)
| 현재 (결재선 관리) | 이전 후 (widgets) |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------ |
| `pages/approval-line-management/ui/ognz-user-tree.tsx` | `widgets/ognz-user-tree/ui/ognz-user-tree.tsx` |
| `pages/.../ui/ognz-user-tree-node.tsx` | `widgets/ognz-user-tree/ui/ognz-user-tree-node.tsx` |
| `pages/.../model/reviewer-option.ts` | `widgets/ognz-user-tree/model/selected-ognz-user.ts` (이름·필드 확장: `ognzSn` 필수) |
| `pages/.../lib/filter-ognz-user-tree.ts` | `widgets/ognz-user-tree/lib/filter-ognz-user-tree.ts` (**필수** — 검색 공통화) |
| `widgets/ognz-user-tree/index.ts` | `OgnzUserTree`, `OgnzUserTreeNode`, 타입 export |
결재선 `AprvrPickerDialog`는 `@/widgets/ognz-user-tree` import로 교체. 검토인력관리 페이지는 **인라인 조직도**(다이얼로그 없음)로 동일 위젯 사용.
**`SelectedOgnzUser`** — `ReviewerOption` 확장:
```typescript
export interface SelectedOgnzUser {
userUuid: string; // OgnzUserTreeUserDto.userUnqId
userNm: string;
ognzSn: number;
ognzNm: string;
jbgdSeCd?: string;
jbpsSeCd?: string;
}
```
### 7-3. 영역별 UI 매핑
| 영역 | 주요 UI |
| ---- | -------------------------------------------------------------------- |
| A-1 | 검색 폼 (`Input` + 조회/초기화) — `AprvrPickerDialog` 90~103행 패턴 |
| A-2 | `OgnzUserTree(nodes={displayTree})` + 선택 안내 + `useOgnzUserTree` |
| B-k | 제목·Badge(count)·`선택 인원 배치`·배치 목록(또는 empty) |
| 공통 | 페이지 헤더·브레드크럼·2열 레이아웃 (`grid` / `flex`, 모바일은 스택) |
### 7-4. 다이얼로그·라우팅
| 항목 | 내용 |
| ---------- | ----------------------------------- |
| 다이얼로그 | **없음** (조직도 인라인, 즉시 저장) |
| 라우팅 | 메뉴 `검토인력관리` 단일 페이지 |
### 7-5. 데이터 흐름(구현 요약)
- **진입**: `useOgnzUserTree` → `displayTree = data` + `useRvwMnpwMngPage({ page: 0, size: N })` → 4패널.
- **검색**: `keyword` 로컬 상태 → **[조회]** 시 `filterOgnzUserTree` + `collectExpandedOgnzSnIds` → `OgnzUserTree`에 `displayTree` 전달.
- **초기화**: `keyword`·`displayTree`·`expandedOgnzSnIds` 리셋 (`selectedPerson` 유지).
- **선택**: 트리 `onSelect` → `selectedPerson` (반드시 `ognzSn` 포함).
- **배치**: `useSaveRvwMnpwMng().mutate({ atrzSeComCd, ognzSn, userUuid })` → `onSuccess`에서 `rvw-mnpw` query invalidate.
- **삭제**: 목록 항목에서 `useDeleteRvwMnpwMng` — UI 확정 후 연결.
### 7-6. 미확정 / 검증 필요
- [ ] `ATRZ02`(검토2) 별도 블록 필요 여부
- [ ] 권한(역할별 CRUD)
+25 -3
View File
@@ -1,8 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import { PORT, PROXY_HOST, SERVER_API_URL, SITE_ID } from "@/shared/config";
import { AUTH_USER_ID, CLAIM_NAME, PORT, PROXY_HOST, SERVER_API_URL, SITE_ID } from "@/shared/config";
const serverApiUrl = process.env.SERVER_API_URL ?? "http://localhost:3000";
const serverApiUrl = process.env.SERVER_API_URL;
// CSP 헤더 설정 함수 (img-src만)
function addSecurityHeaders(response: NextResponse): NextResponse {
@@ -34,6 +34,28 @@ export function middleware(req: NextRequest) {
return addSecurityHeaders(response);
}
// tus-js-client PATCH 요청: Location 헤더로 /server 없이 반환된 경로 직접 처리
// (예: /attach-service/api/v1/tus/upload/{id})
if (pathname.startsWith("/attach-service")) {
const requestHeaders = new Headers(req.headers);
const token = req.cookies.get(CLAIM_NAME)?.value;
const userId = req.cookies.get(AUTH_USER_ID)?.value;
const newUrl = `${serverApiUrl}${pathname}`;
if (token) requestHeaders.set(CLAIM_NAME, token);
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
logMessages.push(` [TUS Proxy] Rewritten URL: ${newUrl}`);
logMessages.push("\n--------------------------------\n");
console.log(logMessages.join("\n"));
const response = NextResponse.rewrite(new URL(newUrl), {
headers: requestHeaders,
});
return addSecurityHeaders(response);
}
// if (pathname === "/") {
// const response = NextResponse.redirect(new URL("/dashboard", req.url));
// return addSecurityHeaders(response);
@@ -47,5 +69,5 @@ export function middleware(req: NextRequest) {
}
export const config = {
matcher: ["/server/:path*", "/"],
matcher: ["/server/:path*", "/attach-service/:path*", "/"],
};
-6
View File
@@ -1,6 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+1
View File
@@ -1,6 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
reactStrictMode: true,
transpilePackages: ["@common/assets"],
};
+13 -3
View File
@@ -3,25 +3,35 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "next dev -p 3003",
"build": "next build",
"start": "next start",
"start": "next start -p 3003",
"lint": "next lint",
"lint:fsd": "steiger ."
},
"dependencies": {
"@common/assets": "workspace:*",
"@common/auth": "workspace:*",
"@common/components": "workspace:*",
"@common/network": "workspace:*",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@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",
"next": "15.3.8",
"lucide-react": "^0.474.0",
"next": "15.5.18",
"react-hook-form": "^7.54.2",
"tus-js-client": "^4.3.1",
"zod": "^3.24.2"
},
"devDependencies": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+7 -1
View File
@@ -5,7 +5,9 @@ import { usePathname, useRouter } from "next/navigation";
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";
@@ -18,16 +20,20 @@ export function BaseLayoutWrapper({ children }: BaseLayoutWrapperProps) {
const pathname = usePathname();
const queryClient = useQueryClient();
const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo);
const handleMenuClick = useCallback(
(item: MenuItem) => {
setCurrentMenuInfo(item);
router.push(item.urlPath);
},
[router],
[router, setCurrentMenuInfo],
);
return (
<BaseLayout
queryClient={queryClient}
authService={authService}
currentPath={pathname ?? ""}
handleMenuClick={handleMenuClick}
>
+14 -2
View File
@@ -1,11 +1,15 @@
"use client";
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
import { QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { queryClient } from "@/shared/lib";
import { StoreProvider } from "@/shared/providers";
import { AlertDialog } from "@/shared/ui/alert-dialog";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { AuthWrapper } from "@/entities/auth";
import { MenuWrapper } from "@/entities/menu";
interface ProvidersProps {
children: React.ReactNode;
@@ -15,9 +19,17 @@ export function Providers({ children }: ProvidersProps) {
return (
<QueryClientProvider client={queryClient}>
<StoreProvider>
<AuthWrapper>{children}</AuthWrapper>
<ConfirmProvider>
<AuthWrapper>
<MenuWrapper>{children}</MenuWrapper>
<AlertDialog />
<ConfirmDialog />
</AuthWrapper>
</ConfirmProvider>
</StoreProvider>
<ReactQueryDevtools initialIsOpen={false} />
<div style={{ fontSize: "1.4rem" }}>
<ReactQueryDevtools initialIsOpen={false} />
</div>
</QueryClientProvider>
);
}
@@ -0,0 +1,58 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import {
ACNTG_SE_SAVE_MODE,
type AcntgSeMngPutRequestDto,
type AcntgSeMngResponseDto,
type AcntgSeMngSaveRequestDto,
type AcntgSeMngSearchDto,
type AcntgSeMngService,
type AcntgSeMngTreeDto,
} from "../model/acntg-interface";
const BASE = ENDPOINT.AS_SERVICE.ACNTG_SE;
async function findAll(params: AcntgSeMngSearchDto): Promise<PageResponse<AcntgSeMngResponseDto>> {
const response = await client.get<PageResponse<AcntgSeMngResponseDto>>(BASE, { params });
return response.data;
}
async function findTree(): Promise<AcntgSeMngTreeDto[]> {
const response = await client.get<AcntgSeMngTreeDto[]>(`${BASE}/list/tree`);
return response.data;
}
async function findById(acntgSeCd: string): Promise<AcntgSeMngResponseDto> {
const response = await client.get<AcntgSeMngResponseDto>(`${BASE}/${acntgSeCd}`);
return response.data;
}
async function saveMulti(body: AcntgSeMngSaveRequestDto[]): Promise<AcntgSeMngResponseDto[]> {
const response = await client.post<AcntgSeMngResponseDto[]>(BASE, body);
return response.data;
}
async function update(acntgSeCd: string, body: AcntgSeMngPutRequestDto): Promise<string> {
const response = await client.put<string>(`${BASE}/${acntgSeCd}`, {
...body,
acntgSeCd,
mode: ACNTG_SE_SAVE_MODE.UPDATE,
});
return response.data;
}
async function deleteAcntgSeCd(acntgSeCd: string): Promise<string> {
const response = await client.delete<string>(`${BASE}/${acntgSeCd}`);
return response.data;
}
export const acntgSeMngService = {
findAll,
findTree,
findById,
saveMulti,
update,
delete: deleteAcntgSeCd,
} satisfies AcntgSeMngService;
+73
View File
@@ -0,0 +1,73 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import type {
AcntgSeMngPutRequestDto,
AcntgSeMngSaveRequestDto,
AcntgSeMngSearchDto,
} from "../model/acntg-interface";
import { acntgSeMngService } from "./acntg-service";
const acntgSeMngBaseKey = [ENDPOINT.AS_SERVICE.ACNTG_SE] as const;
export function useAcntgSeMngPage(params: AcntgSeMngSearchDto) {
return useQuery({
queryKey: [...acntgSeMngBaseKey, "findAll", params],
queryFn: () => acntgSeMngService.findAll(params),
});
}
export function useAcntgSeMngTree() {
return useQuery({
queryKey: [...acntgSeMngBaseKey, "findTree"],
queryFn: () => acntgSeMngService.findTree(),
staleTime: Infinity,
});
}
export function useAcntgSeMng(acntgSeCd: string | null | undefined) {
return useQuery({
queryKey: [...acntgSeMngBaseKey, "findById", acntgSeCd],
queryFn: () => acntgSeMngService.findById(acntgSeCd!),
enabled: Boolean(acntgSeCd),
});
}
export function useSaveMultiAcntgSeMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (body: AcntgSeMngSaveRequestDto[]) => acntgSeMngService.saveMulti(body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: acntgSeMngBaseKey });
},
});
}
export function useUpdateAcntgSeMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ acntgSeCd, body }: { acntgSeCd: string; body: AcntgSeMngPutRequestDto }) =>
acntgSeMngService.update(acntgSeCd, body),
onSuccess: (_data, { acntgSeCd }) => {
queryClient.invalidateQueries({ queryKey: acntgSeMngBaseKey });
queryClient.invalidateQueries({
queryKey: [...acntgSeMngBaseKey, "findById", acntgSeCd],
});
},
});
}
export function useDeleteAcntgSeMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (acntgSeCd: string) => acntgSeMngService.delete(acntgSeCd),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: acntgSeMngBaseKey });
},
});
}
+19
View File
@@ -0,0 +1,19 @@
export { acntgSeMngService } from "./api/acntg-service";
export {
useAcntgSeMng,
useAcntgSeMngPage,
useAcntgSeMngTree,
useDeleteAcntgSeMng,
useSaveMultiAcntgSeMng,
useUpdateAcntgSeMng,
} from "./api/queries";
export type {
AcntgSeMngResponseDto,
AcntgSeMngSaveRequestDto,
AcntgSeMngSearchDto,
AcntgSeMngService,
AcntgSeMngTreeDto,
AcntgSeSaveMode,
BizCstTreeDto,
} from "./model/acntg-interface";
export { ACNTG_SE_SAVE_MODE } from "./model/acntg-interface";
@@ -0,0 +1,88 @@
import type { PageableParams, PageResponse } from "@/shared/model";
export const ACNTG_SE_SAVE_MODE = {
CREATE: "C",
UPDATE: "U",
DELETE: "D",
} as const;
export type AcntgSeSaveMode = (typeof ACNTG_SE_SAVE_MODE)[keyof typeof ACNTG_SE_SAVE_MODE];
/** 백엔드 AcntgSeMngResponseDto */
export interface AcntgSeMngResponseDto {
acntgSeCd: string;
acntgSeCdNm: string;
upAcntgSeCd?: string;
cdLvNo: number;
useYn: string;
expln?: string;
sortSeq?: number;
rgtr?: string;
rgtrNm?: string;
mdfr?: string;
regDt?: string;
mdfcnDt?: string;
}
/** 백엔드 AcntgSeMngSearchDto + Pageable */
export interface AcntgSeMngSearchDto extends PageableParams {
acntgSeCd?: string;
acntgSeCdNm?: string;
useYn?: string;
}
/** 백엔드 AcntgSeMngSaveRequestDto */
export interface AcntgSeMngSaveRequestDto {
acntgSeCd: string;
mode: AcntgSeSaveMode;
acntgSeCdNm: string;
upAcntgSeCd?: string;
cdLvNo: number;
useYn?: string;
expln?: string;
sortSeq?: number;
}
/** PUT /{acntgSeCd} 요청 (mode는 서비스에서 U로 설정) */
export interface AcntgSeMngPutRequestDto {
acntgSeCd: string;
acntgSeCdNm: string;
upAcntgSeCd?: string;
cdLvNo: number;
useYn?: string;
expln?: string;
}
/** 백엔드 AcntgSeMngTreeDto */
export interface AcntgSeMngTreeDto {
indAcntgSeCdNm: string;
acntgSeCd: string;
acntgSeCdNm: string;
upAcntgSeCd?: string;
useYn: string;
treeLevel: number;
treePath: string;
}
/** 백엔드 BizCstTreeDto (InstlSncnMngDetResponseDto 등에서 참조) */
export interface BizCstTreeDto {
indAcntgSeCdNm: string;
acntgSeCd: string;
acntgSeCdNm: string;
useYn: string;
treeLevel: number;
treePath: string;
bscPlanAmt?: number;
bgtRfltAmt?: number;
tstmDesignAmt?: number;
}
/** Controller public 메서드명·시그니처와 동일 */
export interface AcntgSeMngService {
findAll(params: AcntgSeMngSearchDto): Promise<PageResponse<AcntgSeMngResponseDto>>;
findTree(): Promise<AcntgSeMngTreeDto[]>;
findById(acntgSeCd: string): Promise<AcntgSeMngResponseDto>;
saveMulti(body: AcntgSeMngSaveRequestDto[]): Promise<AcntgSeMngResponseDto[]>;
update(acntgSeCd: string, body: AcntgSeMngPutRequestDto): Promise<string>;
delete(acntgSeCd: string): Promise<string>;
}
@@ -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>;
}
@@ -0,0 +1,92 @@
import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config";
import {
type AuthPayload,
type AuthService,
clearDeviceId,
getOrCreateDeviceId,
} from "@common/auth";
import { clearRefreshTimer, setToken, 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",
"X-Device-Id": getOrCreateDeviceId(),
},
body: JSON.stringify(payload),
});
const data = await response.json();
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
throw new Error("인증 토큰을 받지 못했습니다.");
}
setToken(CLAIM_NAME, accessToken);
setToken(AUTH_USER_ID, tokenId);
// const remainingMs = getRemainingMs(accessToken);
// if (remainingMs > 0) {
// setupRefreshTimer(silentRefresh, remainingMs);
// }
}
async function silentRefresh(): Promise<void> {
console.log(`[silentRefresh] Called at ${new Date().toISOString()}`);
try {
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`, {
method: "PUT",
headers: {
"X-Device-Id": getOrCreateDeviceId(),
},
});
const data = await response.json();
if (!response.ok) {
throw new Error(data?.message ?? "토큰 갱신에 실패했습니다.");
}
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
throw new Error("갱신된 인증 토큰을 받지 못했습니다.");
}
setToken(CLAIM_NAME, accessToken);
setToken(AUTH_USER_ID, tokenId);
// const remainingMs = getRemainingMs(accessToken);
// if (remainingMs > 0) {
// setupRefreshTimer(silentRefresh, remainingMs);
// }
} catch (error) {
console.error(`[silentRefresh] Failed at ${new Date().toISOString()}`, error);
tokenManager.clearAuthToken();
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("auth:logout"));
}
}
}
async function logout(): Promise<void> {
tokenManager.clearAuthToken();
clearDeviceId();
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("auth:logout"));
}
}
export const authService: AuthService = {
login,
silentRefresh,
declineSessionExtend: clearRefreshTimer,
logout,
};
@@ -1,92 +0,0 @@
import { AxiosError } from "axios";
import {
ACCESS_TOKEN,
AUTH_USER_ID,
CLAIM_NAME,
ENDPOINT,
JWT_EXPIRATION_TIME,
} from "@/shared/config";
import { tokenManager } from "../lib/token-manager";
import type { ILogin, LoginService } from "../model/login-interface";
export class LoginApiService implements LoginService {
private static instance: LoginApiService;
private constructor() {}
public static getInstance(): LoginApiService {
if (!LoginApiService.instance) {
LoginApiService.instance = new LoginApiService();
}
return LoginApiService.instance;
}
public async login(payload: ILogin): Promise<void> {
try {
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
method: "POST",
body: JSON.stringify({
userId: payload.userId,
password: payload.password,
}),
});
const data = await response.json();
console.log("data ", data);
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
if (data.message == "Invalid Credentials") {
throw new Error("아이디와 비밀번호가 일치하지 않습니다.");
} else if (data.message == "User not found") {
throw new Error("존재하지 않는 아이디입니다.");
} else if (data.message == "User is not active") {
throw new Error("활성화되지 않은 계정입니다.");
} else if (data.message == "User is not verified") {
throw new Error("이메일 인증이 필요합니다.");
} else {
throw new Error(data.message);
}
}
tokenManager.setToken(CLAIM_NAME, accessToken);
tokenManager.setToken(AUTH_USER_ID, tokenId);
tokenManager.setupRefreshTimer(() => this.silentRefresh(), JWT_EXPIRATION_TIME);
} catch (error) {
if (error instanceof AxiosError) {
throw new Error(`Login 실패 ${error.response?.data?.message}`);
}
throw new Error(`Login 실패 ${error}`);
}
}
public async silentRefresh(): Promise<void> {
try {
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`, {
method: "PUT",
});
const data = await response.json();
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
throw new Error("Missing required authentication tokens");
}
tokenManager.setToken(CLAIM_NAME, accessToken);
tokenManager.setToken(AUTH_USER_ID, tokenId);
} catch (error) {
tokenManager.clearAuthToken();
if (error instanceof AxiosError) {
throw new Error(`Silent refresh failed: ${error.response?.data?.message}`);
}
throw new Error(`Silent refresh failed: ${error}`);
}
}
}
export const LoginApi = LoginApiService.getInstance();
+2 -1
View File
@@ -1,2 +1,3 @@
export { LoginApi } from "./api/login-service";
export { authService } from "./api/auth-service";
export { AuthWrapper } from "./lib/auth-wrapper";
export { tokenManager } from "./lib/token-manager";
+56 -27
View File
@@ -1,42 +1,71 @@
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { isAxiosError } from "axios";
import { client } from "@/shared/api";
import { AUTH_LOGIN_REDIRECT_URL, AUTH_USER_ID, ENDPOINT } from "@/shared/config";
import { getCookie, useUserStore } from "@/shared/lib";
import { AUTH_LOGIN_REDIRECT_URL, AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config";
import { useUserStore } from "@/shared/lib";
import type { IUserInfo } from "@/shared/store";
import { getRemainingMs } from "@common/auth";
import { isAtLoginRedirectUrl } from "@common/network";
import { authService } from "../api/auth-service";
import { getToken, setupRefreshTimer } from "./token-manager";
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
const { userInfo, setUserInfo } = useUserStore((state) => state);
async function authCheck() {
if (isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) {
return;
}
const userId = getCookie(AUTH_USER_ID);
if (!userId) {
throw new Error("Missing required authentication tokens");
}
const response = await client.get<IUserInfo>(`${ENDPOINT.USER_SERVICE.USERS}/info/me`);
if (!userInfo) setUserInfo(response.data);
}
const router = useRouter();
const userInfo = useUserStore((state) => state.userInfo);
const setUserInfo = useUserStore((state) => state.setUserInfo);
useEffect(() => {
(async () => {
try {
await authCheck();
} catch (error) {
console.error(error);
setUserInfo(null);
if (!isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) {
window.location.assign(AUTH_LOGIN_REDIRECT_URL);
}
const token = getToken(CLAIM_NAME);
if (token) {
const remainingMs = getRemainingMs(token);
if (remainingMs > 0) {
console.log(`[AuthWrapper] Refresh timer set: ${remainingMs}ms (${remainingMs / 1000}s)`);
setupRefreshTimer(authService.silentRefresh, remainingMs);
}
})();
}
if (isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) return;
async function authCheck() {
const userId = getToken(AUTH_USER_ID);
if (!userId) {
router.replace(AUTH_LOGIN_REDIRECT_URL);
return;
}
if (userInfo) return;
const response = await client.get<IUserInfo>(`${ENDPOINT.USER_SERVICE.USERS}/info/me`);
setUserInfo(response.data);
}
authCheck().catch((error) => {
if (isAxiosError(error) && error.response?.status === 401) return;
console.error(error);
setUserInfo(null);
if (!isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) {
router.replace(AUTH_LOGIN_REDIRECT_URL);
}
});
}, []);
useEffect(() => {
function handleAuthLogout() {
setUserInfo(null);
if (!isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) {
router.replace(AUTH_LOGIN_REDIRECT_URL);
}
}
window.addEventListener("auth:logout", handleAuthLogout);
return () => window.removeEventListener("auth:logout", handleAuthLogout);
}, []);
return <>{children}</>;
+13 -80
View File
@@ -1,82 +1,15 @@
import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config";
import { getCookie, removeCookie,setCookie } from "@/shared/lib";
import { authConfig } from "@/shared/config/auth";
import { createTokenManager } from "@common/auth";
export class TokenManager {
private static instance: TokenManager;
private refreshTimer: NodeJS.Timeout | null = null;
export const tokenManager = createTokenManager(authConfig);
private constructor() {}
public static getInstance(): TokenManager {
if (!TokenManager.instance) {
TokenManager.instance = new TokenManager();
}
return TokenManager.instance;
}
public getToken(key: string): string | undefined {
return getCookie(key);
}
public setToken(key: string, value: string): void {
const domain = this.getCookieDomain();
setCookie(key, value, {
secure: window.location.protocol === "https:",
sameSite: "lax",
domain: domain,
path: "/", // 모든 경로에서 접근 가능
});
}
private getCookieDomain(): string | undefined {
const hostname = window.location.hostname;
// 개발 환경에서는 localhost 도메인 사용
if (hostname === "localhost" || hostname === "127.0.0.1") {
return undefined; // localhost에서는 도메인 설정하지 않음
}
// 운영 환경에서는 서브도메인을 포함한 최상위 도메인 사용
if (hostname.includes("hasuon.or.kr")) {
return ".hasuon.or.kr";
}
// 기타 환경에서는 현재 호스트명 사용
return hostname;
}
public removeToken(key: string): void {
const domain = this.getCookieDomain();
removeCookie(key, { domain, path: "/" });
}
public setupRefreshTimer(
callback: () => Promise<void>,
expirationTime: number,
): void {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
}
this.refreshTimer = setTimeout(() => {
callback().catch((error) => {
console.error("Silent refresh failed:", error);
this.clearAuthToken();
});
}, expirationTime - 60000);
}
public clearAuthToken(): void {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
this.refreshTimer = null;
}
this.removeToken(CLAIM_NAME);
this.removeToken(AUTH_USER_ID);
}
}
export const tokenManager = TokenManager.getInstance();
export const {
getToken,
setToken,
removeToken,
setupRefreshTimer,
clearRefreshTimer,
getAccessToken,
getRefreshToken,
getUserId,
} = tokenManager;
@@ -1,9 +0,0 @@
export interface ILogin {
userId: string;
password: string;
}
export interface LoginService {
login(payload: ILogin): Promise<void>;
silentRefresh(): 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>;
}
@@ -0,0 +1,57 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type {
CklstArtcl,
CklstArtclId,
CklstArtclSaveRequest,
CklstArtclSearchParams,
CklstArtclService,
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>>(BASE, { params });
return response.data;
}
async function findByCklstSn(cklstSn: number): Promise<CklstArtcl[]> {
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>(`${BASE}/${cklstArtclSn}/${cklstSn}`);
return response.data;
}
async function create(body: CklstArtclSaveRequest): Promise<CklstArtclId> {
const response = await client.post<CklstArtclId>(BASE, body);
return response.data;
}
async function update(
cklstArtclSn: number,
cklstSn: number,
body: CklstArtclUpdateRequest,
): Promise<CklstArtclId> {
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>(`${BASE}/${cklstArtclSn}/${cklstSn}`);
return response.data;
}
export const cklstArtclService = {
findPage,
findByCklstSn,
findById,
create,
update,
remove,
} satisfies CklstArtclService;
@@ -0,0 +1,57 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type {
CklstMng,
CklstMngSaveRequest,
CklstMngSearchParams,
CklstMngService,
CklstTreeNode,
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>>(BASE, {
params,
});
return response.data;
}
async function findById(cklstSn: number): Promise<CklstMng> {
const response = await client.get<CklstMng>(`${BASE}/${cklstSn}`);
return response.data;
}
async function findTree(params: CklstTreeSearchParams): Promise<CklstTreeNode[]> {
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>(BASE, body);
return response.data;
}
async function update(cklstSn: number, body: CklstMngSaveRequest): Promise<number> {
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>(`${BASE}/${cklstSn}`);
return response.data;
}
export const cklstMngService = {
findPage,
findById,
findTree,
create,
update,
remove,
} satisfies CklstMngService;
@@ -0,0 +1,71 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type {
CklstRvw,
CklstRvwId,
CklstRvwSaveRequest,
CklstRvwSearchParams,
CklstRvwService,
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>>(BASE, { params });
return response.data;
}
async function findByArtcl(cklstSn: number, cklstArtclSn: number): Promise<CklstRvw[]> {
const response = await client.get<CklstRvw[]>(`${BASE}/by-artcl/${cklstSn}/${cklstArtclSn}`);
return response.data;
}
async function findById(
cklstRvwSn: number,
cklstArtclSn: number,
cklstSn: number,
): Promise<CklstRvw> {
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>(BASE, body);
return response.data;
}
async function update(
cklstRvwSn: number,
cklstArtclSn: number,
cklstSn: number,
body: CklstRvwUpdateRequest,
): Promise<CklstRvwId> {
const response = await client.put<CklstRvwId>(
`${BASE}/${cklstRvwSn}/${cklstArtclSn}/${cklstSn}`,
body,
);
return response.data;
}
async function remove(
cklstRvwSn: number,
cklstArtclSn: number,
cklstSn: number,
): Promise<CklstRvwId> {
const response = await client.delete<CklstRvwId>(
`${BASE}/${cklstRvwSn}/${cklstArtclSn}/${cklstSn}`,
);
return response.data;
}
export const cklstRvwService = {
findPage,
findByArtcl,
findById,
create,
update,
remove,
} satisfies CklstRvwService;
+281
View File
@@ -0,0 +1,281 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import type {
CklstArtclSaveRequest,
CklstArtclSearchParams,
CklstArtclUpdateRequest,
} from "../model/cklst-artcl-interface";
import type { CklstMngSaveRequest, CklstMngSearchParams } from "../model/cklst-mng-interface";
import type {
CklstRvwSaveRequest,
CklstRvwSearchParams,
CklstRvwUpdateRequest,
} from "../model/cklst-rvw-interface";
import { cklstArtclService } from "./cklst-artcl-service";
import { cklstMngService } from "./cklst-mng-service";
import { cklstRvwService } from "./cklst-rvw-service";
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({
queryKey: [...cklstMngBaseKey, "page", params],
queryFn: () => cklstMngService.findPage(params),
});
}
export function useCklstMng(cklstSn: number | null | undefined) {
return useQuery({
queryKey: [...cklstMngBaseKey, cklstSn],
queryFn: () => cklstMngService.findById(cklstSn!),
enabled: cklstSn != null,
});
}
export function useCreateCklstMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (body: CklstMngSaveRequest) => cklstMngService.create(body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: cklstMngBaseKey });
},
});
}
export function useUpdateCklstMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ cklstSn, body }: { cklstSn: number; body: CklstMngSaveRequest }) =>
cklstMngService.update(cklstSn, body),
onSuccess: (_data, { cklstSn }) => {
queryClient.invalidateQueries({ queryKey: cklstMngBaseKey });
queryClient.invalidateQueries({
queryKey: [...cklstMngBaseKey, cklstSn],
});
},
});
}
export function useDeleteCklstMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (cklstSn: number) => cklstMngService.remove(cklstSn),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: cklstMngBaseKey });
},
});
}
export function useCklstArtclPage(params: CklstArtclSearchParams) {
const { cklstSn } = params;
return useQuery({
queryKey: [...cklstArtclBaseKey, "page", params],
queryFn: () => cklstArtclService.findPage(params),
enabled: cklstSn != null,
});
}
export function useCklstArtclList(cklstSn: number | null | undefined) {
return useQuery({
queryKey: [...cklstArtclBaseKey, "by-cklst", cklstSn],
queryFn: () => cklstArtclService.findByCklstSn(cklstSn!),
enabled: cklstSn != null,
});
}
export function useCklstArtcl(params: {
cklstArtclSn: number | null | undefined;
cklstSn: number | null | undefined;
}) {
const { cklstArtclSn, cklstSn } = params;
return useQuery({
queryKey: [...cklstArtclBaseKey, cklstArtclSn, cklstSn],
queryFn: () => cklstArtclService.findById(cklstArtclSn!, cklstSn!),
enabled: cklstArtclSn != null && cklstSn != null,
});
}
export function useCreateCklstArtcl() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (body: CklstArtclSaveRequest) => cklstArtclService.create(body),
onSuccess: (_data, { cklstSn }) => {
queryClient.invalidateQueries({ queryKey: cklstArtclBaseKey });
queryClient.invalidateQueries({
queryKey: [...cklstArtclBaseKey, "page", { cklstSn }],
});
queryClient.invalidateQueries({
queryKey: [...cklstArtclBaseKey, "by-cklst", cklstSn],
});
},
});
}
export function useUpdateCklstArtcl() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
cklstArtclSn,
cklstSn,
body,
}: {
cklstArtclSn: number;
cklstSn: number;
body: CklstArtclUpdateRequest;
}) => cklstArtclService.update(cklstArtclSn, cklstSn, body),
onSuccess: (_data, { cklstArtclSn, cklstSn }) => {
queryClient.invalidateQueries({ queryKey: cklstArtclBaseKey });
queryClient.invalidateQueries({
queryKey: [...cklstArtclBaseKey, cklstArtclSn, cklstSn],
});
queryClient.invalidateQueries({
queryKey: [...cklstArtclBaseKey, "page", { cklstSn }],
});
queryClient.invalidateQueries({
queryKey: [...cklstArtclBaseKey, "by-cklst", cklstSn],
});
},
});
}
export function useDeleteCklstArtcl() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
cklstArtclSn,
cklstSn,
}: {
cklstArtclSn: number;
cklstSn: number;
}) => cklstArtclService.remove(cklstArtclSn, cklstSn),
onSuccess: (_data, { cklstSn }) => {
queryClient.invalidateQueries({ queryKey: cklstArtclBaseKey });
queryClient.invalidateQueries({
queryKey: [...cklstArtclBaseKey, "page", { cklstSn }],
});
queryClient.invalidateQueries({
queryKey: [...cklstArtclBaseKey, "by-cklst", cklstSn],
});
},
});
}
export function useCklstRvwPage(params: CklstRvwSearchParams) {
const { cklstSn } = params;
return useQuery({
queryKey: [...cklstRvwBaseKey, "page", params],
queryFn: () => cklstRvwService.findPage(params),
enabled: cklstSn != null,
});
}
export function useCklstRvwByArtcl(
cklstSn: number | null | undefined,
cklstArtclSn: number | null | undefined,
) {
return useQuery({
queryKey: [...cklstRvwBaseKey, "by-artcl", cklstSn, cklstArtclSn],
queryFn: () => cklstRvwService.findByArtcl(cklstSn!, cklstArtclSn!),
enabled: cklstSn != null && cklstArtclSn != null,
});
}
export function useCklstRvw(params: {
cklstRvwSn: number | null | undefined;
cklstArtclSn: number | null | undefined;
cklstSn: number | null | undefined;
}) {
const { cklstRvwSn, cklstArtclSn, cklstSn } = params;
return useQuery({
queryKey: [...cklstRvwBaseKey, cklstRvwSn, cklstArtclSn, cklstSn],
queryFn: () => cklstRvwService.findById(cklstRvwSn!, cklstArtclSn!, cklstSn!),
enabled: cklstRvwSn != null && cklstArtclSn != null && cklstSn != null,
});
}
export function useCreateCklstRvw() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (body: CklstRvwSaveRequest) => cklstRvwService.create(body),
onSuccess: (_data, { cklstSn, cklstArtclSn }) => {
queryClient.invalidateQueries({ queryKey: cklstRvwBaseKey });
queryClient.invalidateQueries({
queryKey: [...cklstRvwBaseKey, "page", { cklstSn }],
});
queryClient.invalidateQueries({
queryKey: [...cklstRvwBaseKey, "by-artcl", cklstSn, cklstArtclSn],
});
},
});
}
export function useUpdateCklstRvw() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
cklstRvwSn,
cklstArtclSn,
cklstSn,
body,
}: {
cklstRvwSn: number;
cklstArtclSn: number;
cklstSn: number;
body: CklstRvwUpdateRequest;
}) => cklstRvwService.update(cklstRvwSn, cklstArtclSn, cklstSn, body),
onSuccess: (_data, { cklstRvwSn, cklstArtclSn, cklstSn }) => {
queryClient.invalidateQueries({ queryKey: cklstRvwBaseKey });
queryClient.invalidateQueries({
queryKey: [...cklstRvwBaseKey, cklstRvwSn, cklstArtclSn, cklstSn],
});
queryClient.invalidateQueries({
queryKey: [...cklstRvwBaseKey, "page", { cklstSn }],
});
queryClient.invalidateQueries({
queryKey: [...cklstRvwBaseKey, "by-artcl", cklstSn, cklstArtclSn],
});
},
});
}
export function useDeleteCklstRvw() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
cklstRvwSn,
cklstArtclSn,
cklstSn,
}: {
cklstRvwSn: number;
cklstArtclSn: number;
cklstSn: number;
}) => cklstRvwService.remove(cklstRvwSn, cklstArtclSn, cklstSn),
onSuccess: (_data, { cklstSn, cklstArtclSn }) => {
queryClient.invalidateQueries({ queryKey: cklstRvwBaseKey });
queryClient.invalidateQueries({
queryKey: [...cklstRvwBaseKey, "page", { cklstSn }],
});
queryClient.invalidateQueries({
queryKey: [...cklstRvwBaseKey, "by-artcl", cklstSn, cklstArtclSn],
});
},
});
}
+46
View File
@@ -0,0 +1,46 @@
export { cklstArtclService } from "./api/cklst-artcl-service";
export { cklstMngService } from "./api/cklst-mng-service";
export { cklstRvwService } from "./api/cklst-rvw-service";
export {
useCklstArtcl,
useCklstArtclList,
useCklstArtclPage,
useCklstMng,
useCklstMngPage,
useCklstRvw,
useCklstRvwByArtcl,
useCklstRvwPage,
useCreateCklstArtcl,
useCreateCklstMng,
useCreateCklstRvw,
useDeleteCklstArtcl,
useDeleteCklstMng,
useDeleteCklstRvw,
useUpdateCklstArtcl,
useUpdateCklstMng,
useUpdateCklstRvw,
} from "./api/queries";
export type {
CklstArtcl,
CklstArtclId,
CklstArtclSaveRequest,
CklstArtclSearchParams,
CklstArtclService,
CklstArtclUpdateRequest,
} from "./model/cklst-artcl-interface";
export type {
CklstMng,
CklstMngSaveRequest,
CklstMngSearchParams,
CklstMngService,
CklstTreeNode,
CklstTreeSearchParams,
} from "./model/cklst-mng-interface";
export type {
CklstRvw,
CklstRvwId,
CklstRvwSaveRequest,
CklstRvwSearchParams,
CklstRvwService,
CklstRvwUpdateRequest,
} from "./model/cklst-rvw-interface";
@@ -0,0 +1,48 @@
import type { PageableParams, PageResponse } from "@/shared/model";
export interface CklstArtclId {
cklstArtclSn: number;
cklstSn: number;
}
export interface CklstArtcl {
cklstArtclSn: number;
cklstSn: number;
cklstArtclNm?: string;
useYn?: "Y" | "N";
rgtr?: string;
rgtrNm?: string;
mdfr?: string;
regDt?: string;
mdfcnDt?: string;
}
export interface CklstArtclSearchParams extends PageableParams {
cklstSn?: number;
cklstArtclNm?: string;
useYn?: "Y" | "N";
}
export interface CklstArtclSaveRequest {
cklstSn: number;
cklstArtclNm?: string;
useYn?: "Y" | "N";
}
export interface CklstArtclUpdateRequest {
cklstArtclNm?: string;
useYn?: "Y" | "N";
}
export interface CklstArtclService {
findPage(params: CklstArtclSearchParams): Promise<PageResponse<CklstArtcl>>;
findByCklstSn(cklstSn: number): Promise<CklstArtcl[]>;
findById(cklstArtclSn: number, cklstSn: number): Promise<CklstArtcl>;
create(body: CklstArtclSaveRequest): Promise<CklstArtclId>;
update(
cklstArtclSn: number,
cklstSn: number,
body: CklstArtclUpdateRequest,
): Promise<CklstArtclId>;
remove(cklstArtclSn: number, cklstSn: number): Promise<CklstArtclId>;
}
@@ -0,0 +1,57 @@
import type { PageableParams, PageResponse } from "@/shared/model";
export interface CklstMng {
cklstSn: number;
cklstNm: string;
taskSeComCd: string;
useYn?: "Y" | "N";
docSeComCd?: string;
rgtr?: string;
rgtrNm?: string;
mdfr?: string;
regDt?: string;
mdfcnDt?: string;
}
export interface CklstTreeNode {
cklstSn: number;
cklstNm: string;
cklstArtclSn: number;
cklstArtclNm: string;
cklstRvwSn: number;
upCklstRvwSn: number | null;
treeLevel: number;
rvwCn: string;
bchckUseYn: "Y" | "N";
sortSeq: number;
// optional: backend may include the taskSeComCd per node
taskSeComCd?: string;
}
export interface CklstTreeSearchParams {
docSeComCd?: string;
taskSeComCd?: string;
}
export interface CklstMngSearchParams extends PageableParams {
cklstNm?: string;
taskSeComCd?: string;
useYn?: "Y" | "N";
docSeComCd?: string;
}
export interface CklstMngSaveRequest {
cklstNm: string;
taskSeComCd: string;
useYn?: "Y" | "N";
docSeComCd?: string;
}
export interface CklstMngService {
findPage(params: CklstMngSearchParams): Promise<PageResponse<CklstMng>>;
findById(cklstSn: number): Promise<CklstMng>;
findTree(params: CklstTreeSearchParams): Promise<CklstTreeNode[]>;
create(body: CklstMngSaveRequest): Promise<number>;
update(cklstSn: number, body: CklstMngSaveRequest): Promise<number>;
remove(cklstSn: number): Promise<number>;
}
@@ -0,0 +1,64 @@
import type { PageableParams, PageResponse } from "@/shared/model";
export interface CklstRvwId {
cklstRvwSn: number;
cklstArtclSn: number;
cklstSn: number;
}
export interface CklstRvw {
cklstRvwSn: number;
cklstArtclSn: number;
cklstSn: number;
upCklstRvwSn?: number;
rvwCnLvNo?: number;
rvwCn?: string;
bchckUseYn?: "Y" | "N";
sortSeq?: number;
useYn?: "Y" | "N";
rgtr?: string;
rgtrNm?: string;
mdfr?: string;
regDt?: string;
mdfcnDt?: string;
}
export interface CklstRvwSearchParams extends PageableParams {
cklstSn?: number;
cklstArtclSn?: number;
useYn?: "Y" | "N";
}
export interface CklstRvwSaveRequest {
cklstArtclSn: number;
cklstSn: number;
upCklstRvwSn?: number;
rvwCnLvNo?: number;
rvwCn?: string;
bchckUseYn?: "Y" | "N";
sortSeq?: number;
useYn?: "Y" | "N";
}
export interface CklstRvwUpdateRequest {
upCklstRvwSn?: number;
rvwCnLvNo?: number;
rvwCn?: string;
bchckUseYn?: "Y" | "N";
sortSeq?: number;
useYn?: "Y" | "N";
}
export interface CklstRvwService {
findPage(params: CklstRvwSearchParams): Promise<PageResponse<CklstRvw>>;
findByArtcl(cklstSn: number, cklstArtclSn: number): Promise<CklstRvw[]>;
findById(cklstRvwSn: number, cklstArtclSn: number, cklstSn: number): Promise<CklstRvw>;
create(body: CklstRvwSaveRequest): Promise<CklstRvwId>;
update(
cklstRvwSn: number,
cklstArtclSn: number,
cklstSn: number,
body: CklstRvwUpdateRequest,
): Promise<CklstRvwId>;
remove(cklstRvwSn: number, cklstArtclSn: number, cklstSn: number): Promise<CklstRvwId>;
}
@@ -0,0 +1,58 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import {
CNSTP_SE_SAVE_MODE,
type CnstpSeMngPutRequestDto,
type CnstpSeMngResponseDto,
type CnstpSeMngSaveRequestDto,
type CnstpSeMngSearchDto,
type CnstpSeMngService,
type CnstpSeMngTreeDto,
} from "../model/cnstp-interface";
const BASE = ENDPOINT.AS_SERVICE.CNSTP_SE;
async function findAll(params: CnstpSeMngSearchDto): Promise<PageResponse<CnstpSeMngResponseDto>> {
const response = await client.get<PageResponse<CnstpSeMngResponseDto>>(BASE, { params });
return response.data;
}
async function findTree(): Promise<CnstpSeMngTreeDto[]> {
const response = await client.get<CnstpSeMngTreeDto[]>(`${BASE}/list/tree`);
return response.data;
}
async function findById(cnstpSeCd: string): Promise<CnstpSeMngResponseDto> {
const response = await client.get<CnstpSeMngResponseDto>(`${BASE}/${cnstpSeCd}`);
return response.data;
}
async function saveMulti(body: CnstpSeMngSaveRequestDto[]): Promise<CnstpSeMngResponseDto[]> {
const response = await client.post<CnstpSeMngResponseDto[]>(BASE, body);
return response.data;
}
async function update(cnstpSeCd: string, body: CnstpSeMngPutRequestDto): Promise<string> {
const response = await client.put<string>(`${BASE}/${cnstpSeCd}`, {
...body,
cnstpSeCd,
mode: CNSTP_SE_SAVE_MODE.UPDATE,
});
return response.data;
}
async function deleteCnstpSeCd(cnstpSeCd: string): Promise<string> {
const response = await client.delete<string>(`${BASE}/${cnstpSeCd}`);
return response.data;
}
export const cnstpSeMngService = {
findAll,
findTree,
findById,
saveMulti,
update,
delete: deleteCnstpSeCd,
} satisfies CnstpSeMngService;
+73
View File
@@ -0,0 +1,73 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import type {
CnstpSeMngPutRequestDto,
CnstpSeMngSaveRequestDto,
CnstpSeMngSearchDto,
} from "../model/cnstp-interface";
import { cnstpSeMngService } from "./cnstp-service";
const cnstpSeMngBaseKey = [ENDPOINT.AS_SERVICE.CNSTP_SE] as const;
export function useCnstpSeMngPage(params: CnstpSeMngSearchDto) {
return useQuery({
queryKey: [...cnstpSeMngBaseKey, "findAll", params],
queryFn: () => cnstpSeMngService.findAll(params),
});
}
export function useCnstpSeMngTree() {
return useQuery({
queryKey: [...cnstpSeMngBaseKey, "findTree"],
queryFn: () => cnstpSeMngService.findTree(),
staleTime: Infinity,
});
}
export function useCnstpSeMng(cnstpSeCd: string | null | undefined) {
return useQuery({
queryKey: [...cnstpSeMngBaseKey, "findById", cnstpSeCd],
queryFn: () => cnstpSeMngService.findById(cnstpSeCd!),
enabled: Boolean(cnstpSeCd),
});
}
export function useSaveMultiCnstpSeMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (body: CnstpSeMngSaveRequestDto[]) => cnstpSeMngService.saveMulti(body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: cnstpSeMngBaseKey });
},
});
}
export function useUpdateCnstpSeMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ cnstpSeCd, body }: { cnstpSeCd: string; body: CnstpSeMngPutRequestDto }) =>
cnstpSeMngService.update(cnstpSeCd, body),
onSuccess: (_data, { cnstpSeCd }) => {
queryClient.invalidateQueries({ queryKey: cnstpSeMngBaseKey });
queryClient.invalidateQueries({
queryKey: [...cnstpSeMngBaseKey, "findById", cnstpSeCd],
});
},
});
}
export function useDeleteCnstpSeMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (cnstpSeCd: string) => cnstpSeMngService.delete(cnstpSeCd),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: cnstpSeMngBaseKey });
},
});
}
+19
View File
@@ -0,0 +1,19 @@
export { cnstpSeMngService } from "./api/cnstp-service";
export {
useCnstpSeMng,
useCnstpSeMngPage,
useCnstpSeMngTree,
useDeleteCnstpSeMng,
useSaveMultiCnstpSeMng,
useUpdateCnstpSeMng,
} from "./api/queries";
export type {
BizSclTreeDto,
CnstpSeMngResponseDto,
CnstpSeMngSaveRequestDto,
CnstpSeMngSearchDto,
CnstpSeMngService,
CnstpSeMngTreeDto,
CnstpSeSaveMode,
} from "./model/cnstp-interface";
export { CNSTP_SE_SAVE_MODE } from "./model/cnstp-interface";
@@ -0,0 +1,88 @@
import type { PageableParams, PageResponse } from "@/shared/model";
export const CNSTP_SE_SAVE_MODE = {
CREATE: "C",
UPDATE: "U",
DELETE: "D",
} as const;
export type CnstpSeSaveMode = (typeof CNSTP_SE_SAVE_MODE)[keyof typeof CNSTP_SE_SAVE_MODE];
/** 백엔드 CnstpSeMngResponseDto */
export interface CnstpSeMngResponseDto {
cnstpSeCd: string;
cnstpSeCdNm: string;
upCnstpCd?: string;
cdLvNo: number;
useYn: string;
expln?: string;
sortSeq?: number;
rgtr?: string;
rgtrNm?: string;
mdfr?: string;
regDt?: string;
mdfcnDt?: string;
}
/** 백엔드 CnstpSeMngSearchDto + Pageable */
export interface CnstpSeMngSearchDto extends PageableParams {
cnstpSeCd?: string;
cnstpSeCdNm?: string;
useYn?: string;
}
/** 백엔드 CnstpSeMngSaveRequestDto */
export interface CnstpSeMngSaveRequestDto {
cnstpSeCd: string;
mode: CnstpSeSaveMode;
cnstpSeCdNm?: string;
upCnstpCd?: string;
cdLvNo?: number;
useYn?: string;
expln?: string;
sortSeq?: number;
}
/** PUT /{cnstpSeCd} 요청 (mode는 서비스에서 U로 설정) */
export interface CnstpSeMngPutRequestDto {
cnstpSeCd: string;
cnstpSeCdNm?: string;
upCnstpCd?: string;
cdLvNo?: number;
useYn?: string;
expln?: string;
}
/** 백엔드 CnstpSeMngTreeDto */
export interface CnstpSeMngTreeDto {
indCnstpSeCdNm: string;
cnstpSeCd: string;
cnstpSeCdNm: string;
upCnstpCd?: string;
useYn: string;
treeLevel: number;
treePath: string;
}
/** 백엔드 BizSclTreeDto */
export interface BizSclTreeDto {
indCnstpSeCdNm: string;
cnstpSeCd: string;
cnstpSeCdNm: string;
useYn: string;
treeLevel: number;
treePath: string;
bscPlanQty?: number;
bgtRfltQty?: number;
tstmDesignQty?: number;
}
/** Controller public 메서드명·시그니처와 동일 */
export interface CnstpSeMngService {
findAll(params: CnstpSeMngSearchDto): Promise<PageResponse<CnstpSeMngResponseDto>>;
findTree(): Promise<CnstpSeMngTreeDto[]>;
findById(cnstpSeCd: string): Promise<CnstpSeMngResponseDto>;
saveMulti(body: CnstpSeMngSaveRequestDto[]): Promise<CnstpSeMngResponseDto[]>;
update(cnstpSeCd: string, body: CnstpSeMngPutRequestDto): Promise<string>;
delete(cnstpSeCd: string): Promise<string>;
}
@@ -0,0 +1,27 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { Code, CodeService } from "../model/code-interface";
async function getList(upCd: string): Promise<Code[]> {
const response = await client.get<Code[]>(`${ENDPOINT.CMS_SERVICE.CODE}/${upCd}`);
return response.data;
}
async function getListByUseYn(upCd: string, useYn: string): Promise<Code[]> {
const response = await client.get<Code[]>(`${ENDPOINT.CMS_SERVICE.CODE}/${upCd}/${useYn}`);
return response.data;
}
async function getCodeName(upCd: string, lwpoCd: string): Promise<string> {
const response = await client.get<string>(
`${ENDPOINT.CMS_SERVICE.CODE}/${upCd}/lwpoCd/${lwpoCd}`,
);
return response.data;
}
export const codeService = {
getList,
getListByUseYn,
getCodeName,
} satisfies CodeService;
+36
View File
@@ -0,0 +1,36 @@
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import { codeService } from "./code-service";
const codeBaseKey = [ENDPOINT.CMS_SERVICE.CODE] as const;
const CODE_STALE_TIME = 1000 * 60 * 10;
export function useCodeList(upCd: string | undefined) {
return useQuery({
queryKey: [...codeBaseKey, upCd],
queryFn: () => codeService.getList(upCd!),
enabled: Boolean(upCd),
staleTime: CODE_STALE_TIME,
});
}
export function useCodeListByUseYn(upCd: string | undefined, useYn: string) {
return useQuery({
queryKey: [...codeBaseKey, upCd, useYn],
queryFn: () => codeService.getListByUseYn(upCd!, useYn),
enabled: Boolean(upCd && useYn),
staleTime: CODE_STALE_TIME,
});
}
export function useCodeName(upCd: string | undefined, lwpoCd: string | undefined) {
return useQuery({
queryKey: [...codeBaseKey, upCd, "name", lwpoCd],
queryFn: () => codeService.getCodeName(upCd!, lwpoCd!),
enabled: Boolean(upCd && lwpoCd),
staleTime: CODE_STALE_TIME,
});
}
+5
View File
@@ -0,0 +1,5 @@
export { codeService } from "./api/code-service";
export { useCodeList, useCodeListByUseYn, useCodeName } from "./api/queries";
export { findCodeLabel } from "./lib/find-code-label";
export type { Code, CodeService } from "./model/code-interface";
export { CodeSelect } from "./ui/code-select";
@@ -0,0 +1,7 @@
import type { Code } from "../model/code-interface";
export function findCodeLabel(codes: Code[] | undefined, lwpoCd?: string): string {
if (!lwpoCd) return "";
if (!codes?.length) return lwpoCd;
return codes.find((c) => c.lwpoCd === lwpoCd)?.lwpoCdNm ?? lwpoCd;
}
@@ -0,0 +1,14 @@
/** cms-service CodeResponseDto */
export interface Code {
lwpoCd: string;
upCd: string;
lwpoCdNm: string;
unit?: string;
prntLwpoCd?: string;
}
export interface CodeService {
getList(upCd: string): Promise<Code[]>;
getListByUseYn(upCd: string, useYn: string): Promise<Code[]>;
getCodeName(upCd: string, lwpoCd: string): Promise<string>;
}
@@ -0,0 +1,44 @@
"use client";
import {
cn,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@common/components";
import { useCodeList, useCodeListByUseYn } from "../api/queries";
interface CodeSelectProps extends React.ComponentPropsWithoutRef<typeof Select> {
upCd: string;
useYn?: string;
placeholder?: string;
className?: string;
}
function CodeSelect({ upCd, useYn, placeholder = "선택", className, ...props }: CodeSelectProps) {
const listQuery = useCodeList(useYn ? undefined : upCd);
const listByUseYnQuery = useCodeListByUseYn(useYn ? upCd : undefined, useYn ?? "");
const codes = useYn ? listByUseYnQuery.data : listQuery.data;
const isLoading = useYn ? listByUseYnQuery.isPending : listQuery.isPending;
return (
<Select {...props} disabled={props.disabled || isLoading}>
<SelectTrigger className={cn("w-[12rem]", className)}>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
{codes?.map((item) => (
<SelectItem key={item.lwpoCd} value={item.lwpoCd}>
{item.lwpoCdNm}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
export { CodeSelect };
@@ -0,0 +1,58 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type {
DesignCnstnBizCstId,
DesignCnstnBizCstResponseDto,
DesignCnstnBizCstSaveRequestDto,
DesignCnstnBizCstSearchDto,
DesignCnstnBizCstService,
DesignCnstnBizCstUpdateRequestDto,
} from "../model/design-cnstn-biz-cst-interface";
const BASE = ENDPOINT.AS_SERVICE.DESIGN_CNSTN_BIZ_CST;
async function findPage(
searchDto: DesignCnstnBizCstSearchDto,
): Promise<PageResponse<DesignCnstnBizCstResponseDto>> {
const response = await client.get<PageResponse<DesignCnstnBizCstResponseDto>>(BASE, {
params: searchDto,
});
return response.data;
}
async function findById(
designCnstnDocNo: string,
acntgSeCd: string,
): Promise<DesignCnstnBizCstResponseDto> {
const response = await client.get<DesignCnstnBizCstResponseDto>(
`${BASE}/${designCnstnDocNo}/${acntgSeCd}`,
);
return response.data;
}
async function save(body: DesignCnstnBizCstSaveRequestDto): Promise<DesignCnstnBizCstId> {
const response = await client.post<DesignCnstnBizCstId>(BASE, body);
return response.data;
}
async function update(
designCnstnDocNo: string,
acntgSeCd: string,
body: DesignCnstnBizCstUpdateRequestDto,
): Promise<void> {
await client.put<void>(`${BASE}/${designCnstnDocNo}/${acntgSeCd}`, body);
}
async function deleteDesignCnstnBizCst(designCnstnDocNo: string, acntgSeCd: string): Promise<void> {
await client.delete<void>(`${BASE}/${designCnstnDocNo}/${acntgSeCd}`);
}
export const designCnstnBizCstService = {
findPage,
findById,
save,
update,
delete: deleteDesignCnstnBizCst,
} satisfies DesignCnstnBizCstService;
@@ -0,0 +1,58 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type {
DesignCnstnBizSclId,
DesignCnstnBizSclResponseDto,
DesignCnstnBizSclSaveRequestDto,
DesignCnstnBizSclSearchDto,
DesignCnstnBizSclService,
DesignCnstnBizSclUpdateRequestDto,
} from "../model/design-cnstn-biz-scl-interface";
const BASE = ENDPOINT.AS_SERVICE.DESIGN_CNSTN_BIZ_SCL;
async function findPage(
searchDto: DesignCnstnBizSclSearchDto,
): Promise<PageResponse<DesignCnstnBizSclResponseDto>> {
const response = await client.get<PageResponse<DesignCnstnBizSclResponseDto>>(BASE, {
params: searchDto,
});
return response.data;
}
async function findById(
designCnstnDocNo: string,
cnstpSeCd: string,
): Promise<DesignCnstnBizSclResponseDto> {
const response = await client.get<DesignCnstnBizSclResponseDto>(
`${BASE}/${designCnstnDocNo}/${cnstpSeCd}`,
);
return response.data;
}
async function save(body: DesignCnstnBizSclSaveRequestDto): Promise<DesignCnstnBizSclId> {
const response = await client.post<DesignCnstnBizSclId>(BASE, body);
return response.data;
}
async function update(
designCnstnDocNo: string,
cnstpSeCd: string,
body: DesignCnstnBizSclUpdateRequestDto,
): Promise<void> {
await client.put<void>(`${BASE}/${designCnstnDocNo}/${cnstpSeCd}`, body);
}
async function deleteDesignCnstnBizScl(designCnstnDocNo: string, cnstpSeCd: string): Promise<void> {
await client.delete<void>(`${BASE}/${designCnstnDocNo}/${cnstpSeCd}`);
}
export const designCnstnBizSclService = {
findPage,
findById,
save,
update,
delete: deleteDesignCnstnBizScl,
} satisfies DesignCnstnBizSclService;
@@ -0,0 +1,63 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type {
DesignCnstnEsntlAtchFileId,
DesignCnstnEsntlAtchFileResponseDto,
DesignCnstnEsntlAtchFileSaveRequestDto,
DesignCnstnEsntlAtchFileSearchDto,
DesignCnstnEsntlAtchFileService,
DesignCnstnEsntlAtchFileUpdateRequestDto,
} from "../model/design-cnstn-esntl-atch-file-interface";
const BASE = ENDPOINT.AS_SERVICE.DESIGN_CNSTN_ESNTL_ATCH_FILE;
async function findPage(
searchDto: DesignCnstnEsntlAtchFileSearchDto,
): Promise<PageResponse<DesignCnstnEsntlAtchFileResponseDto>> {
const response = await client.get<PageResponse<DesignCnstnEsntlAtchFileResponseDto>>(BASE, {
params: searchDto,
});
return response.data;
}
async function findById(
designCnstnDocNo: string,
esntlAtchFileId: string,
): Promise<DesignCnstnEsntlAtchFileResponseDto> {
const response = await client.get<DesignCnstnEsntlAtchFileResponseDto>(
`${BASE}/${designCnstnDocNo}/${esntlAtchFileId}`,
);
return response.data;
}
async function save(
body: DesignCnstnEsntlAtchFileSaveRequestDto,
): Promise<DesignCnstnEsntlAtchFileId> {
const response = await client.post<DesignCnstnEsntlAtchFileId>(BASE, body);
return response.data;
}
async function update(
designCnstnDocNo: string,
esntlAtchFileId: string,
body: DesignCnstnEsntlAtchFileUpdateRequestDto,
): Promise<void> {
await client.put<void>(`${BASE}/${designCnstnDocNo}/${esntlAtchFileId}`, body);
}
async function deleteDesignCnstnEsntlAtchFile(
designCnstnDocNo: string,
esntlAtchFileId: string,
): Promise<void> {
await client.delete<void>(`${BASE}/${designCnstnDocNo}/${esntlAtchFileId}`);
}
export const designCnstnEsntlAtchFileService = {
findPage,
findById,
save,
update,
delete: deleteDesignCnstnEsntlAtchFile,
} satisfies DesignCnstnEsntlAtchFileService;
@@ -0,0 +1,61 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type {
DesignCnstnApplySaveRequestDto,
DesignCnstnMngDetResponseDto,
DesignCnstnMngResponseDto,
DesignCnstnMngSaveRequestDto,
DesignCnstnMngSearchDto,
DesignCnstnMngService,
DesignCnstnMngUpdateRequestDto,
} from "../model/design-cnstn-mng-interface";
const BASE = ENDPOINT.AS_SERVICE.DESIGN_CNSTN;
async function findPage(
searchDto: DesignCnstnMngSearchDto,
): Promise<PageResponse<DesignCnstnMngResponseDto>> {
const response = await client.get<PageResponse<DesignCnstnMngResponseDto>>(BASE, {
params: searchDto,
});
return response.data;
}
async function findById(id: string): Promise<DesignCnstnMngDetResponseDto> {
const response = await client.get<DesignCnstnMngDetResponseDto>(`${BASE}/${id}`);
return response.data;
}
async function save(body: DesignCnstnMngSaveRequestDto): Promise<string> {
const response = await client.post<string>(BASE, body);
return response.data;
}
async function saveApply(body: DesignCnstnApplySaveRequestDto): Promise<string> {
const response = await client.post<string>(`${BASE}/save-apply`, body);
return response.data;
}
async function update(id: string, body: DesignCnstnMngUpdateRequestDto): Promise<void> {
await client.put<void>(`${BASE}/${id}`, body);
}
async function techReview(id: string, body: DesignCnstnApplySaveRequestDto): Promise<void> {
await client.post<void>(`${BASE}/${id}/tech-review`, body);
}
async function deleteDesignCnstnMng(id: string): Promise<void> {
await client.delete<void>(`${BASE}/${id}`);
}
export const designCnstnMngService = {
findPage,
findById,
save,
saveApply,
update,
techReview,
delete: deleteDesignCnstnMng,
} satisfies DesignCnstnMngService;
@@ -0,0 +1,61 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type {
DesignCnstnRvwOpnnId,
DesignCnstnRvwOpnnResponseDto,
DesignCnstnRvwOpnnSaveRequestDto,
DesignCnstnRvwOpnnSearchDto,
DesignCnstnRvwOpnnService,
DesignCnstnRvwOpnnUpdateRequestDto,
} from "../model/design-cnstn-rvw-opnn-interface";
const BASE = ENDPOINT.AS_SERVICE.DESIGN_CNSTN_RVW_OPNN;
async function findPage(
searchDto: DesignCnstnRvwOpnnSearchDto,
): Promise<PageResponse<DesignCnstnRvwOpnnResponseDto>> {
const response = await client.get<PageResponse<DesignCnstnRvwOpnnResponseDto>>(BASE, {
params: searchDto,
});
return response.data;
}
async function findById(
designCnstnDocNo: string,
rvwOpnnSn: number,
): Promise<DesignCnstnRvwOpnnResponseDto> {
const response = await client.get<DesignCnstnRvwOpnnResponseDto>(
`${BASE}/${designCnstnDocNo}/${rvwOpnnSn}`,
);
return response.data;
}
async function save(body: DesignCnstnRvwOpnnSaveRequestDto): Promise<DesignCnstnRvwOpnnId> {
const response = await client.post<DesignCnstnRvwOpnnId>(BASE, body);
return response.data;
}
async function update(
designCnstnDocNo: string,
rvwOpnnSn: number,
body: DesignCnstnRvwOpnnUpdateRequestDto,
): Promise<void> {
await client.put<void>(`${BASE}/${designCnstnDocNo}/${rvwOpnnSn}`, body);
}
async function deleteDesignCnstnRvwOpnn(
designCnstnDocNo: string,
rvwOpnnSn: number,
): Promise<void> {
await client.delete<void>(`${BASE}/${designCnstnDocNo}/${rvwOpnnSn}`);
}
export const designCnstnRvwOpnnService = {
findPage,
findById,
save,
update,
delete: deleteDesignCnstnRvwOpnn,
} satisfies DesignCnstnRvwOpnnService;
@@ -0,0 +1,411 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import type {
DesignCnstnBizCstSaveRequestDto,
DesignCnstnBizCstSearchDto,
DesignCnstnBizCstUpdateRequestDto,
} from "../model/design-cnstn-biz-cst-interface";
import type {
DesignCnstnBizSclSaveRequestDto,
DesignCnstnBizSclSearchDto,
DesignCnstnBizSclUpdateRequestDto,
} from "../model/design-cnstn-biz-scl-interface";
import type {
DesignCnstnEsntlAtchFileSaveRequestDto,
DesignCnstnEsntlAtchFileSearchDto,
DesignCnstnEsntlAtchFileUpdateRequestDto,
} from "../model/design-cnstn-esntl-atch-file-interface";
import type {
DesignCnstnApplySaveRequestDto,
DesignCnstnMngSaveRequestDto,
DesignCnstnMngSearchDto,
DesignCnstnMngUpdateRequestDto,
} from "../model/design-cnstn-mng-interface";
import type {
DesignCnstnRvwOpnnSaveRequestDto,
DesignCnstnRvwOpnnSearchDto,
DesignCnstnRvwOpnnUpdateRequestDto,
} from "../model/design-cnstn-rvw-opnn-interface";
import { designCnstnBizCstService } from "./design-cnstn-biz-cst-service";
import { designCnstnBizSclService } from "./design-cnstn-biz-scl-service";
import { designCnstnEsntlAtchFileService } from "./design-cnstn-esntl-atch-file-service";
import { designCnstnMngService } from "./design-cnstn-mng-service";
import { designCnstnRvwOpnnService } from "./design-cnstn-rvw-opnn-service";
export const designCnstnMngBaseKey = [ENDPOINT.AS_SERVICE.DESIGN_CNSTN] as const;
export const designCnstnBizCstBaseKey = [ENDPOINT.AS_SERVICE.DESIGN_CNSTN_BIZ_CST] as const;
export const designCnstnBizSclBaseKey = [ENDPOINT.AS_SERVICE.DESIGN_CNSTN_BIZ_SCL] as const;
export const designCnstnEsntlAtchFileBaseKey = [
ENDPOINT.AS_SERVICE.DESIGN_CNSTN_ESNTL_ATCH_FILE,
] as const;
export const designCnstnRvwOpnnBaseKey = [ENDPOINT.AS_SERVICE.DESIGN_CNSTN_RVW_OPNN] as const;
/* ─── DesignCnstnMng ─── */
export function useDesignCnstnMngPage(params: DesignCnstnMngSearchDto) {
return useQuery({
queryKey: [...designCnstnMngBaseKey, "findPage", params],
queryFn: () => designCnstnMngService.findPage(params),
});
}
export function useDesignCnstnMng(id: string | null | undefined) {
return useQuery({
queryKey: [...designCnstnMngBaseKey, "findById", id],
queryFn: () => designCnstnMngService.findById(id!),
enabled: Boolean(id),
});
}
export function useSaveDesignCnstnMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (body: DesignCnstnMngSaveRequestDto) => designCnstnMngService.save(body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
},
});
}
export function useSaveApplyDesignCnstnMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (body: DesignCnstnApplySaveRequestDto) => designCnstnMngService.saveApply(body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
},
});
}
export function useUpdateDesignCnstnMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, body }: { id: string; body: DesignCnstnMngUpdateRequestDto }) =>
designCnstnMngService.update(id, body),
onSuccess: (_data, { id }) => {
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
queryClient.invalidateQueries({
queryKey: [...designCnstnMngBaseKey, "findById", id],
});
},
});
}
export function useTechReviewDesignCnstnMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, body }: { id: string; body: DesignCnstnApplySaveRequestDto }) =>
designCnstnMngService.techReview(id, body),
onSuccess: (_data, { id }) => {
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
queryClient.invalidateQueries({
queryKey: [...designCnstnMngBaseKey, "findById", id],
});
},
});
}
export function useDeleteDesignCnstnMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => designCnstnMngService.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
},
});
}
/* ─── DesignCnstnBizCst ─── */
export function useDesignCnstnBizCstPage(params: DesignCnstnBizCstSearchDto) {
return useQuery({
queryKey: [...designCnstnBizCstBaseKey, "findPage", params],
queryFn: () => designCnstnBizCstService.findPage(params),
});
}
export function useDesignCnstnBizCst(
designCnstnDocNo: string | null | undefined,
acntgSeCd: string | null | undefined,
) {
return useQuery({
queryKey: [...designCnstnBizCstBaseKey, "findById", designCnstnDocNo, acntgSeCd],
queryFn: () => designCnstnBizCstService.findById(designCnstnDocNo!, acntgSeCd!),
enabled: Boolean(designCnstnDocNo && acntgSeCd),
});
}
export function useSaveDesignCnstnBizCst() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (body: DesignCnstnBizCstSaveRequestDto) => designCnstnBizCstService.save(body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: designCnstnBizCstBaseKey });
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
},
});
}
export function useUpdateDesignCnstnBizCst() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
designCnstnDocNo,
acntgSeCd,
body,
}: {
designCnstnDocNo: string;
acntgSeCd: string;
body: DesignCnstnBizCstUpdateRequestDto;
}) => designCnstnBizCstService.update(designCnstnDocNo, acntgSeCd, body),
onSuccess: (_data, { designCnstnDocNo, acntgSeCd }) => {
queryClient.invalidateQueries({ queryKey: designCnstnBizCstBaseKey });
queryClient.invalidateQueries({
queryKey: [...designCnstnBizCstBaseKey, "findById", designCnstnDocNo, acntgSeCd],
});
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
},
});
}
export function useDeleteDesignCnstnBizCst() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
designCnstnDocNo,
acntgSeCd,
}: {
designCnstnDocNo: string;
acntgSeCd: string;
}) => designCnstnBizCstService.delete(designCnstnDocNo, acntgSeCd),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: designCnstnBizCstBaseKey });
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
},
});
}
/* ─── DesignCnstnBizScl ─── */
export function useDesignCnstnBizSclPage(params: DesignCnstnBizSclSearchDto) {
return useQuery({
queryKey: [...designCnstnBizSclBaseKey, "findPage", params],
queryFn: () => designCnstnBizSclService.findPage(params),
});
}
export function useDesignCnstnBizScl(
designCnstnDocNo: string | null | undefined,
cnstpSeCd: string | null | undefined,
) {
return useQuery({
queryKey: [...designCnstnBizSclBaseKey, "findById", designCnstnDocNo, cnstpSeCd],
queryFn: () => designCnstnBizSclService.findById(designCnstnDocNo!, cnstpSeCd!),
enabled: Boolean(designCnstnDocNo && cnstpSeCd),
});
}
export function useSaveDesignCnstnBizScl() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (body: DesignCnstnBizSclSaveRequestDto) => designCnstnBizSclService.save(body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: designCnstnBizSclBaseKey });
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
},
});
}
export function useUpdateDesignCnstnBizScl() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
designCnstnDocNo,
cnstpSeCd,
body,
}: {
designCnstnDocNo: string;
cnstpSeCd: string;
body: DesignCnstnBizSclUpdateRequestDto;
}) => designCnstnBizSclService.update(designCnstnDocNo, cnstpSeCd, body),
onSuccess: (_data, { designCnstnDocNo, cnstpSeCd }) => {
queryClient.invalidateQueries({ queryKey: designCnstnBizSclBaseKey });
queryClient.invalidateQueries({
queryKey: [...designCnstnBizSclBaseKey, "findById", designCnstnDocNo, cnstpSeCd],
});
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
},
});
}
export function useDeleteDesignCnstnBizScl() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
designCnstnDocNo,
cnstpSeCd,
}: {
designCnstnDocNo: string;
cnstpSeCd: string;
}) => designCnstnBizSclService.delete(designCnstnDocNo, cnstpSeCd),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: designCnstnBizSclBaseKey });
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
},
});
}
/* ─── DesignCnstnEsntlAtchFile ─── */
export function useDesignCnstnEsntlAtchFilePage(params: DesignCnstnEsntlAtchFileSearchDto) {
return useQuery({
queryKey: [...designCnstnEsntlAtchFileBaseKey, "findPage", params],
queryFn: () => designCnstnEsntlAtchFileService.findPage(params),
});
}
export function useDesignCnstnEsntlAtchFile(
designCnstnDocNo: string | null | undefined,
esntlAtchFileId: string | null | undefined,
) {
return useQuery({
queryKey: [
...designCnstnEsntlAtchFileBaseKey,
"findById",
designCnstnDocNo,
esntlAtchFileId,
],
queryFn: () =>
designCnstnEsntlAtchFileService.findById(designCnstnDocNo!, esntlAtchFileId!),
enabled: Boolean(designCnstnDocNo && esntlAtchFileId),
});
}
export function useSaveDesignCnstnEsntlAtchFile() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (body: DesignCnstnEsntlAtchFileSaveRequestDto) =>
designCnstnEsntlAtchFileService.save(body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: designCnstnEsntlAtchFileBaseKey });
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
},
});
}
export function useUpdateDesignCnstnEsntlAtchFile() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
designCnstnDocNo,
esntlAtchFileId,
body,
}: {
designCnstnDocNo: string;
esntlAtchFileId: string;
body: DesignCnstnEsntlAtchFileUpdateRequestDto;
}) => designCnstnEsntlAtchFileService.update(designCnstnDocNo, esntlAtchFileId, body),
onSuccess: (_data, { designCnstnDocNo, esntlAtchFileId }) => {
queryClient.invalidateQueries({ queryKey: designCnstnEsntlAtchFileBaseKey });
queryClient.invalidateQueries({
queryKey: [
...designCnstnEsntlAtchFileBaseKey,
"findById",
designCnstnDocNo,
esntlAtchFileId,
],
});
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
},
});
}
export function useDeleteDesignCnstnEsntlAtchFile() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
designCnstnDocNo,
esntlAtchFileId,
}: {
designCnstnDocNo: string;
esntlAtchFileId: string;
}) => designCnstnEsntlAtchFileService.delete(designCnstnDocNo, esntlAtchFileId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: designCnstnEsntlAtchFileBaseKey });
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
},
});
}
/* ─── DesignCnstnRvwOpnn ─── */
export function useDesignCnstnRvwOpnnPage(params: DesignCnstnRvwOpnnSearchDto) {
return useQuery({
queryKey: [...designCnstnRvwOpnnBaseKey, "findPage", params],
queryFn: () => designCnstnRvwOpnnService.findPage(params),
});
}
export function useDesignCnstnRvwOpnn(
designCnstnDocNo: string | null | undefined,
rvwOpnnSn: number | null | undefined,
) {
return useQuery({
queryKey: [...designCnstnRvwOpnnBaseKey, "findById", designCnstnDocNo, rvwOpnnSn],
queryFn: () => designCnstnRvwOpnnService.findById(designCnstnDocNo!, rvwOpnnSn!),
enabled: Boolean(designCnstnDocNo && rvwOpnnSn != null),
});
}
export function useSaveDesignCnstnRvwOpnn() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (body: DesignCnstnRvwOpnnSaveRequestDto) => designCnstnRvwOpnnService.save(body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: designCnstnRvwOpnnBaseKey });
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
},
});
}
export function useUpdateDesignCnstnRvwOpnn() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
designCnstnDocNo,
rvwOpnnSn,
body,
}: {
designCnstnDocNo: string;
rvwOpnnSn: number;
body: DesignCnstnRvwOpnnUpdateRequestDto;
}) => designCnstnRvwOpnnService.update(designCnstnDocNo, rvwOpnnSn, body),
onSuccess: (_data, { designCnstnDocNo, rvwOpnnSn }) => {
queryClient.invalidateQueries({ queryKey: designCnstnRvwOpnnBaseKey });
queryClient.invalidateQueries({
queryKey: [...designCnstnRvwOpnnBaseKey, "findById", designCnstnDocNo, rvwOpnnSn],
});
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
},
});
}
export function useDeleteDesignCnstnRvwOpnn() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
designCnstnDocNo,
rvwOpnnSn,
}: {
designCnstnDocNo: string;
rvwOpnnSn: number;
}) => designCnstnRvwOpnnService.delete(designCnstnDocNo, rvwOpnnSn),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: designCnstnRvwOpnnBaseKey });
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
},
});
}
@@ -0,0 +1,88 @@
export { designCnstnBizCstService } from "./api/design-cnstn-biz-cst-service";
export { designCnstnBizSclService } from "./api/design-cnstn-biz-scl-service";
export { designCnstnEsntlAtchFileService } from "./api/design-cnstn-esntl-atch-file-service";
export { designCnstnMngService } from "./api/design-cnstn-mng-service";
export { designCnstnRvwOpnnService } from "./api/design-cnstn-rvw-opnn-service";
export {
designCnstnBizCstBaseKey,
designCnstnBizSclBaseKey,
designCnstnEsntlAtchFileBaseKey,
designCnstnMngBaseKey,
designCnstnRvwOpnnBaseKey,
useDeleteDesignCnstnBizCst,
useDeleteDesignCnstnBizScl,
useDeleteDesignCnstnEsntlAtchFile,
useDeleteDesignCnstnMng,
useDeleteDesignCnstnRvwOpnn,
useDesignCnstnBizCst,
useDesignCnstnBizCstPage,
useDesignCnstnBizScl,
useDesignCnstnBizSclPage,
useDesignCnstnEsntlAtchFile,
useDesignCnstnEsntlAtchFilePage,
useDesignCnstnMng,
useDesignCnstnMngPage,
useDesignCnstnRvwOpnn,
useDesignCnstnRvwOpnnPage,
useSaveApplyDesignCnstnMng,
useSaveDesignCnstnBizCst,
useSaveDesignCnstnBizScl,
useSaveDesignCnstnEsntlAtchFile,
useSaveDesignCnstnMng,
useSaveDesignCnstnRvwOpnn,
useTechReviewDesignCnstnMng,
useUpdateDesignCnstnBizCst,
useUpdateDesignCnstnBizScl,
useUpdateDesignCnstnEsntlAtchFile,
useUpdateDesignCnstnMng,
useUpdateDesignCnstnRvwOpnn,
} from "./api/queries";
export type {
DesignCnstnBizCstId,
DesignCnstnBizCstResponseDto,
DesignCnstnBizCstSaveRequestDto,
DesignCnstnBizCstSearchDto,
DesignCnstnBizCstService,
DesignCnstnBizCstUpdateRequestDto,
} from "./model/design-cnstn-biz-cst-interface";
export type {
DesignCnstnBizSclId,
DesignCnstnBizSclResponseDto,
DesignCnstnBizSclSaveRequestDto,
DesignCnstnBizSclSearchDto,
DesignCnstnBizSclService,
DesignCnstnBizSclUpdateRequestDto,
} from "./model/design-cnstn-biz-scl-interface";
export type {
DesignCnstnEsntlAtchFileId,
DesignCnstnEsntlAtchFileResponseDto,
DesignCnstnEsntlAtchFileSaveRequestDto,
DesignCnstnEsntlAtchFileSearchDto,
DesignCnstnEsntlAtchFileService,
DesignCnstnEsntlAtchFileUpdateRequestDto,
} from "./model/design-cnstn-esntl-atch-file-interface";
export type {
AtflMngClientResponseDto,
BizCstItem,
BizCstTreeDto,
BizSclTreeDto,
BzScaleItem,
DesignCnstnApplySaveRequestDto,
DesignCnstnMngDetResponseDto,
DesignCnstnMngResponseDto,
DesignCnstnMngSaveRequestDto,
DesignCnstnMngSearchDto,
DesignCnstnMngService,
DesignCnstnMngUpdateRequestDto,
DesignCnstnRvwOpnnResponseDto,
DesignCnstnRvwOpnnSaveRequestDto,
ElaprDocPrgrsClientResponseDto,
EsntlAtchFileItem,
EsntlAtchFileMngTreeResponseDto,
} from "./model/design-cnstn-mng-interface";
export type {
DesignCnstnRvwOpnnId,
DesignCnstnRvwOpnnSearchDto,
DesignCnstnRvwOpnnService,
DesignCnstnRvwOpnnUpdateRequestDto,
} from "./model/design-cnstn-rvw-opnn-interface";
@@ -0,0 +1,54 @@
import type { PageableParams, PageResponse } from "@/shared/model";
/** 백엔드 DesignCnstnBizCstId */
export interface DesignCnstnBizCstId {
designCnstnMng: string;
acntgSeCd: string;
}
/** 백엔드 DesignCnstnBizCstSaveRequestDto */
export interface DesignCnstnBizCstSaveRequestDto {
designCnstnDocNo: string;
acntgSeCd: string;
bscPlanAmt?: number;
bgtRfltAmt?: number;
tstmDesignAmt?: number;
}
/** 백엔드 DesignCnstnBizCstUpdateRequestDto */
export interface DesignCnstnBizCstUpdateRequestDto {
bscPlanAmt?: number;
bgtRfltAmt?: number;
tstmDesignAmt?: number;
}
/** 백엔드 DesignCnstnBizCstSearchDto + Pageable */
export interface DesignCnstnBizCstSearchDto extends PageableParams {
designCnstnDocNo?: string;
acntgSeCd?: string;
}
/** 백엔드 DesignCnstnBizCstResponseDto */
export interface DesignCnstnBizCstResponseDto {
designCnstnDocNo: string;
acntgSeCd: string;
bscPlanAmt?: number;
bgtRfltAmt?: number;
tstmDesignAmt?: number;
regDt?: string;
}
/** Controller public 메서드명·시그니처와 동일 */
export interface DesignCnstnBizCstService {
findPage(
searchDto: DesignCnstnBizCstSearchDto,
): Promise<PageResponse<DesignCnstnBizCstResponseDto>>;
findById(designCnstnDocNo: string, acntgSeCd: string): Promise<DesignCnstnBizCstResponseDto>;
save(body: DesignCnstnBizCstSaveRequestDto): Promise<DesignCnstnBizCstId>;
update(
designCnstnDocNo: string,
acntgSeCd: string,
body: DesignCnstnBizCstUpdateRequestDto,
): Promise<void>;
delete(designCnstnDocNo: string, acntgSeCd: string): Promise<void>;
}
@@ -0,0 +1,54 @@
import type { PageableParams, PageResponse } from "@/shared/model";
/** 백엔드 DesignCnstnBizSclId */
export interface DesignCnstnBizSclId {
designCnstnMng: string;
cnstpSeCd: string;
}
/** 백엔드 DesignCnstnBizSclSaveRequestDto */
export interface DesignCnstnBizSclSaveRequestDto {
designCnstnDocNo: string;
cnstpSeCd: string;
bscPlanQty?: number;
bgtRfltQty?: number;
tstmDesignQty?: number;
}
/** 백엔드 DesignCnstnBizSclUpdateRequestDto */
export interface DesignCnstnBizSclUpdateRequestDto {
bscPlanQty?: number;
bgtRfltQty?: number;
tstmDesignQty?: number;
}
/** 백엔드 DesignCnstnBizSclSearchDto + Pageable */
export interface DesignCnstnBizSclSearchDto extends PageableParams {
designCnstnDocNo?: string;
cnstpSeCd?: string;
}
/** 백엔드 DesignCnstnBizSclResponseDto */
export interface DesignCnstnBizSclResponseDto {
designCnstnDocNo: string;
cnstpSeCd: string;
bscPlanQty?: number;
bgtRfltQty?: number;
tstmDesignQty?: number;
regDt?: string;
}
/** Controller public 메서드명·시그니처와 동일 */
export interface DesignCnstnBizSclService {
findPage(
searchDto: DesignCnstnBizSclSearchDto,
): Promise<PageResponse<DesignCnstnBizSclResponseDto>>;
findById(designCnstnDocNo: string, cnstpSeCd: string): Promise<DesignCnstnBizSclResponseDto>;
save(body: DesignCnstnBizSclSaveRequestDto): Promise<DesignCnstnBizSclId>;
update(
designCnstnDocNo: string,
cnstpSeCd: string,
body: DesignCnstnBizSclUpdateRequestDto,
): Promise<void>;
delete(designCnstnDocNo: string, cnstpSeCd: string): Promise<void>;
}
@@ -0,0 +1,51 @@
import type { PageableParams, PageResponse } from "@/shared/model";
/** 백엔드 DesignCnstnEsntlAtchFileId */
export interface DesignCnstnEsntlAtchFileId {
designCnstnMng: string;
esntlAtchFileId: string;
}
/** 백엔드 DesignCnstnEsntlAtchFileSaveRequestDto */
export interface DesignCnstnEsntlAtchFileSaveRequestDto {
designCnstnDocNo: string;
esntlAtchFileId: string;
atchFileGroupId?: string;
}
/** 백엔드 DesignCnstnEsntlAtchFileUpdateRequestDto */
export interface DesignCnstnEsntlAtchFileUpdateRequestDto {
atchFileGroupId?: string;
}
/** 백엔드 DesignCnstnEsntlAtchFileSearchDto + Pageable */
export interface DesignCnstnEsntlAtchFileSearchDto extends PageableParams {
designCnstnDocNo?: string;
esntlAtchFileId?: string;
}
/** 백엔드 DesignCnstnEsntlAtchFileResponseDto */
export interface DesignCnstnEsntlAtchFileResponseDto {
designCnstnDocNo: string;
esntlAtchFileId: string;
atchFileGroupId?: string;
regDt?: string;
}
/** Controller public 메서드명·시그니처와 동일 */
export interface DesignCnstnEsntlAtchFileService {
findPage(
searchDto: DesignCnstnEsntlAtchFileSearchDto,
): Promise<PageResponse<DesignCnstnEsntlAtchFileResponseDto>>;
findById(
designCnstnDocNo: string,
esntlAtchFileId: string,
): Promise<DesignCnstnEsntlAtchFileResponseDto>;
save(body: DesignCnstnEsntlAtchFileSaveRequestDto): Promise<DesignCnstnEsntlAtchFileId>;
update(
designCnstnDocNo: string,
esntlAtchFileId: string,
body: DesignCnstnEsntlAtchFileUpdateRequestDto,
): Promise<void>;
delete(designCnstnDocNo: string, esntlAtchFileId: string): Promise<void>;
}
@@ -0,0 +1,124 @@
import type {
BizCstSaveBase,
BzScaleSaveBase,
EsntlAtchFileSaveBase,
PageableParams,
PageResponse,
PbadmsApplySaveHeader,
PbadmsMngDetResponseBase,
} from "@/shared/model";
import type {
DesignCnstnRvwOpnnResponseDto,
DesignCnstnRvwOpnnSaveRequestDto,
} from "./design-cnstn-rvw-opnn-interface";
export type {
DesignCnstnRvwOpnnResponseDto,
DesignCnstnRvwOpnnSaveRequestDto,
} from "./design-cnstn-rvw-opnn-interface";
export type {
AtflMngClientResponseDto,
BizCstTreeDto,
BizSclTreeDto,
ElaprDocPrgrsClientResponseDto,
EsntlAtchFileMngTreeResponseDto,
} from "@/shared/model";
/** 백엔드 DesignCnstnMngSaveRequestDto */
export interface DesignCnstnMngSaveRequestDto {
designCnstnDocNo?: string;
admdstCd?: string;
envoSeComCd?: string;
taskSeComCd?: 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;
prgrsSttsComCd?: string;
}
/** 백엔드 DesignCnstnMngUpdateRequestDto */
export type DesignCnstnMngUpdateRequestDto = Omit<DesignCnstnMngSaveRequestDto, "designCnstnDocNo">;
/** 백엔드 DesignCnstnMngSearchDto + Pageable */
export interface DesignCnstnMngSearchDto extends PageableParams {
designCnstnDocNo?: string;
admdstCd?: string;
envoSeComCd?: string;
taskSeComCd?: 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;
}
interface DesignCnstnMngResponseExtension {
designCnstnDocNo: string;
envoSeCdNm?: string;
taskSeCdNm?: string;
prgrsSttsCdNm?: string;
regDt?: string;
rgtr?: string;
rgtrNm?: string;
mdfcnDt?: string;
sido?: string;
gugun?: string;
}
/** 백엔드 DesignCnstnMngResponseDto */
export type DesignCnstnMngResponseDto = Omit<DesignCnstnMngSaveRequestDto, "designCnstnDocNo"> &
DesignCnstnMngResponseExtension;
/** 백엔드 DesignCnstnApplySaveRequestDto.BzScaleItem */
export type BzScaleItem = BzScaleSaveBase;
/** 백엔드 DesignCnstnApplySaveRequestDto.BizCstItem */
export type BizCstItem = BizCstSaveBase;
/** 백엔드 DesignCnstnApplySaveRequestDto.EsntlAtchFileItem */
export type EsntlAtchFileItem = EsntlAtchFileSaveBase;
/** 백엔드 DesignCnstnMngDetResponseDto */
export type DesignCnstnMngDetResponseDto = PbadmsMngDetResponseBase<
DesignCnstnMngResponseDto,
DesignCnstnRvwOpnnResponseDto
>;
/** 백엔드 DesignCnstnApplySaveRequestDto */
export interface DesignCnstnApplySaveRequestDto extends PbadmsApplySaveHeader {
designCnstnMng: DesignCnstnMngSaveRequestDto;
bzScaleList?: BzScaleItem[];
bizCstList?: BizCstItem[];
esntlAtchFileList?: EsntlAtchFileItem[];
rvwOpnnList?: DesignCnstnRvwOpnnSaveRequestDto[];
}
/** Controller public 메서드명·시그니처와 동일 */
export interface DesignCnstnMngService {
findPage(searchDto: DesignCnstnMngSearchDto): Promise<PageResponse<DesignCnstnMngResponseDto>>;
findById(id: string): Promise<DesignCnstnMngDetResponseDto>;
save(body: DesignCnstnMngSaveRequestDto): Promise<string>;
saveApply(body: DesignCnstnApplySaveRequestDto): Promise<string>;
update(id: string, body: DesignCnstnMngUpdateRequestDto): Promise<void>;
techReview(id: string, body: DesignCnstnApplySaveRequestDto): Promise<void>;
delete(id: string): Promise<void>;
}
@@ -0,0 +1,59 @@
import type { PageableParams, PageResponse, RvwOpnnResponseExtension } from "@/shared/model";
/** 백엔드 DesignCnstnRvwOpnnId */
export interface DesignCnstnRvwOpnnId {
designCnstnMng: string;
rvwOpnnSn: number;
}
/** 백엔드 DesignCnstnRvwOpnnSaveRequestDto */
export interface DesignCnstnRvwOpnnSaveRequestDto {
designCnstnDocNo: string;
rvwOpnnSn?: number;
opnrSeComCd?: string;
opnrNm?: string;
rvwRsltCn?: string;
rvwRsltAnmlyYn?: string;
atchFileGroupId?: string;
}
/** 백엔드 DesignCnstnRvwOpnnUpdateRequestDto */
export interface DesignCnstnRvwOpnnUpdateRequestDto {
opnrSeComCd?: string;
opnrNm?: string;
rvwRsltCn?: string;
rvwRsltAnmlyYn?: string;
atchFileGroupId?: string;
}
/** 백엔드 DesignCnstnRvwOpnnSearchDto + Pageable */
export interface DesignCnstnRvwOpnnSearchDto extends PageableParams {
designCnstnDocNo?: string;
opnrNm?: string;
opnrSeComCd?: string;
rvwRsltCn?: string;
rvwRsltAnmlyYn?: string;
atchFileGroupId?: string;
}
/** 백엔드 DesignCnstnRvwOpnnResponseDto */
export type DesignCnstnRvwOpnnResponseDto = Partial<
Pick<DesignCnstnRvwOpnnSaveRequestDto, "designCnstnDocNo" | "rvwOpnnSn">
> &
Omit<DesignCnstnRvwOpnnSaveRequestDto, "designCnstnDocNo" | "rvwOpnnSn"> &
RvwOpnnResponseExtension;
/** Controller public 메서드명·시그니처와 동일 */
export interface DesignCnstnRvwOpnnService {
findPage(
searchDto: DesignCnstnRvwOpnnSearchDto,
): Promise<PageResponse<DesignCnstnRvwOpnnResponseDto>>;
findById(designCnstnDocNo: string, rvwOpnnSn: number): Promise<DesignCnstnRvwOpnnResponseDto>;
save(body: DesignCnstnRvwOpnnSaveRequestDto): Promise<DesignCnstnRvwOpnnId>;
update(
designCnstnDocNo: string,
rvwOpnnSn: number,
body: DesignCnstnRvwOpnnUpdateRequestDto,
): Promise<void>;
delete(designCnstnDocNo: string, rvwOpnnSn: number): Promise<void>;
}
@@ -0,0 +1,54 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type {
ElaprApln,
ElaprAplnListParams,
ElaprAplnReorderRequest,
ElaprAplnSaveRequest,
ElaprAplnService,
ElaprAplnUpdateRequest,
} from "../model/elapr-apln-interface";
async function findPage(params: ElaprAplnListParams): Promise<PageResponse<ElaprApln>> {
const response = await client.get<PageResponse<ElaprApln>>(ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN, {
params,
});
return response.data;
}
async function findById(aplnId: string): Promise<ElaprApln> {
const response = await client.get<ElaprApln>(`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN}/${aplnId}`);
return response.data;
}
async function saveMulti(body: ElaprAplnSaveRequest[]): Promise<ElaprApln[]> {
const response = await client.post<ElaprApln[]>(ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN, body);
return response.data;
}
async function update(aplnId: string, body: ElaprAplnUpdateRequest): Promise<ElaprApln> {
const response = await client.put<ElaprApln>(
`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN}/${aplnId}`,
body,
);
return response.data;
}
async function remove(aplnId: string): Promise<void> {
await client.delete(`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN}/${aplnId}`);
}
async function reorder(body: ElaprAplnReorderRequest): Promise<void> {
await client.put(`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN}/reorder`, body);
}
export const elaprAplnService = {
findPage,
findById,
saveMulti,
update,
remove,
reorder,
} satisfies ElaprAplnService;
@@ -0,0 +1,67 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type {
ElaprDmndResponseDto,
ElaprDmndSaveRequestDto,
ElaprDmndSearchDto,
ElaprDmndService,
ElaprDocPrgrsResponseDto,
} from "../model/elapr-dmnd-interface";
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;
}
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;
@@ -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;
@@ -0,0 +1,51 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type {
ElaprTmplt,
ElaprTmpltSaveRequest,
ElaprTmpltSearchParams,
ElaprTmpltService,
ElaprTmpltUpdateRequest,
} from "../model/elapr-tmplt-interface";
async function findPage(params: ElaprTmpltSearchParams): Promise<PageResponse<ElaprTmplt>> {
const response = await client.get<PageResponse<ElaprTmplt>>(
ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT,
{ params },
);
return response.data;
}
async function findById(elaprTmpltId: string): Promise<ElaprTmplt> {
const response = await client.get<ElaprTmplt>(
`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT}/${elaprTmpltId}`,
);
return response.data;
}
async function create(body: ElaprTmpltSaveRequest): Promise<ElaprTmplt> {
const response = await client.post<ElaprTmplt>(ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT, body);
return response.data;
}
async function update(elaprTmpltId: string, body: ElaprTmpltUpdateRequest): Promise<ElaprTmplt> {
const response = await client.put<ElaprTmplt>(
`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT}/${elaprTmpltId}`,
body,
);
return response.data;
}
async function remove(elaprTmpltId: string): Promise<void> {
await client.delete(`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT}/${elaprTmpltId}`);
}
export const elaprTmpltService = {
findPage,
findById,
create,
update,
remove,
} satisfies ElaprTmpltService;
+362
View File
@@ -0,0 +1,362 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import type {
ElaprAplnListParams,
ElaprAplnReorderRequest,
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,
ElaprTmpltUpdateRequest,
} 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({
queryKey: [...elaprTmpltBaseKey, "page", params],
queryFn: () => elaprTmpltService.findPage(params),
});
}
export function useElaprTmplt(elaprTmpltId: string | null | undefined) {
return useQuery({
queryKey: [...elaprTmpltBaseKey, elaprTmpltId],
queryFn: () => elaprTmpltService.findById(elaprTmpltId!),
enabled: Boolean(elaprTmpltId),
});
}
export function useCreateElaprTmplt() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (body: ElaprTmpltSaveRequest) => elaprTmpltService.create(body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: elaprTmpltBaseKey });
},
});
}
export function useUpdateElaprTmplt() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ elaprTmpltId, body }: { elaprTmpltId: string; body: ElaprTmpltUpdateRequest }) =>
elaprTmpltService.update(elaprTmpltId, body),
onSuccess: (_data, { elaprTmpltId }) => {
queryClient.invalidateQueries({ queryKey: elaprTmpltBaseKey });
queryClient.invalidateQueries({
queryKey: [...elaprTmpltBaseKey, elaprTmpltId],
});
},
});
}
export function useDeleteElaprTmplt() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (elaprTmpltId: string) => elaprTmpltService.remove(elaprTmpltId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: elaprTmpltBaseKey });
},
});
}
export function useElaprAplnPage(params: ElaprAplnListParams) {
const { elaprTmpltId } = params;
return useQuery({
queryKey: [...elaprAplnBaseKey, "page", params],
queryFn: () => elaprAplnService.findPage(params),
enabled: Boolean(elaprTmpltId),
});
}
export function useElaprApln(aplnId: string | null | undefined) {
return useQuery({
queryKey: [...elaprAplnBaseKey, aplnId],
queryFn: () => elaprAplnService.findById(aplnId!),
enabled: Boolean(aplnId),
});
}
export function useSaveMultiElaprApln() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ body }: { body: ElaprAplnSaveRequest[]; elaprTmpltId: string }) =>
elaprAplnService.saveMulti(body),
onSuccess: (_data, { elaprTmpltId }) => {
queryClient.invalidateQueries({ queryKey: elaprAplnBaseKey });
queryClient.invalidateQueries({
queryKey: [...elaprAplnBaseKey, "page", { elaprTmpltId }],
});
},
});
}
export function useUpdateElaprApln() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ aplnId, body }: { aplnId: string; body: ElaprAplnUpdateRequest }) =>
elaprAplnService.update(aplnId, body),
onSuccess: (_data, { body, aplnId }) => {
queryClient.invalidateQueries({ queryKey: elaprAplnBaseKey });
queryClient.invalidateQueries({
queryKey: [...elaprAplnBaseKey, aplnId],
});
queryClient.invalidateQueries({
queryKey: [...elaprAplnBaseKey, "page", { elaprTmpltId: body.elaprTmpltId }],
});
},
});
}
export function useDeleteElaprApln() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ aplnId }: { aplnId: string; elaprTmpltId: string }) =>
elaprAplnService.remove(aplnId),
onSuccess: (_data, { elaprTmpltId }) => {
queryClient.invalidateQueries({ queryKey: elaprAplnBaseKey });
queryClient.invalidateQueries({
queryKey: [...elaprAplnBaseKey, "page", { elaprTmpltId }],
});
},
});
}
export function useReorderElaprApln() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ body }: { body: ElaprAplnReorderRequest; elaprTmpltId: string }) =>
elaprAplnService.reorder(body),
onSuccess: (_data, { elaprTmpltId }) => {
queryClient.invalidateQueries({ queryKey: elaprAplnBaseKey });
queryClient.invalidateQueries({
queryKey: [...elaprAplnBaseKey, "page", { elaprTmpltId }],
});
},
});
}
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
View File
@@ -0,0 +1,122 @@
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";
@@ -0,0 +1,97 @@
import {
ELAPR_APLN_SAVE_MODE,
type ElaprApln,
type ElaprAplnSaveRequest,
} from "../model/elapr-apln-interface";
import type { ElaprAplnRow } from "../model/elapr-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 (
row.aprvrUuid !== serverRow.aprvrUuid ||
row.atrzSeComCd !== serverRow.atrzSeComCd ||
row.atrzSeqNo !== serverRow.atrzSeqNo
);
}
function shouldIncludeUpdate(row: ElaprAplnRow, serverRows: ElaprApln[]): boolean {
if (row.isDirty) return true;
if (!row.aplnId) return false;
const serverRow = serverRows.find((s) => s.aplnId === row.aplnId);
if (!serverRow) return false;
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,
serverRows: ElaprApln[],
): ElaprAplnSaveRequest[] {
const payload: ElaprAplnSaveRequest[] = [];
for (const row of rows) {
if (row.isDeleted && row.aplnId) {
payload.push({
mode: ELAPR_APLN_SAVE_MODE.DELETE,
aplnId: row.aplnId,
});
}
}
const visibleWithSeq = assignAtrzSeqNumbers(getVisibleRows(rows));
for (const row of visibleWithSeq) {
if (row.isNew) {
payload.push({
mode: ELAPR_APLN_SAVE_MODE.CREATE,
elaprTmpltId,
aprvrUuid: row.aprvrUuid,
atrzSeComCd: row.atrzSeComCd,
atrzSeqNo: row.atrzSeqNo,
});
continue;
}
if (!row.aplnId) continue;
if (shouldIncludeUpdate(row, serverRows)) {
payload.push({
mode: ELAPR_APLN_SAVE_MODE.UPDATE,
aplnId: row.aplnId,
elaprTmpltId,
aprvrUuid: row.aprvrUuid,
atrzSeComCd: row.atrzSeComCd,
atrzSeqNo: row.atrzSeqNo,
});
}
}
return payload;
}
@@ -0,0 +1,77 @@
import type { ElaprAplnRow } from "../model/elapr-apln-row-types";
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: AtrzOrderValidateRow[]): string | null {
const visibleRows = rows.filter((row) => !row.isDeleted);
const finalCount = visibleRows.filter((row) => row.atrzSeComCd === ATRZ_FINAL_CD).length;
if (finalCount === 0) {
return "최종승인자는 반드시 등록해야 합니다.";
}
if (finalCount > 1) {
return "최종승인자는 한 명만 등록할 수 있습니다.";
}
if (finalCount === 1) {
const lastRow = visibleRows[visibleRows.length - 1];
if (lastRow?.atrzSeComCd !== ATRZ_FINAL_CD) {
return "최종승인자는 결재선 마지막에 배치해야 합니다.";
}
}
return null;
}
export function validateAplnRow(row: ElaprAplnRow): string | null {
if (!row.atrzSeComCd?.trim()) {
return "결재구분을 선택해 주세요.";
}
if (!row.aprvrUuid?.trim()) {
return "결재자를 선택해 주세요.";
}
return null;
}
export function validateAllRowsForSave(
rows: ElaprAplnRow[],
isEditingLocked: boolean,
): string | null {
if (isEditingLocked) {
return "편집 중인 행을 먼저 저장하거나 취소해 주세요.";
}
const visibleRows = getVisibleRows(rows);
if (visibleRows.length === 0) {
return "저장할 결재선이 없습니다.";
}
for (const row of visibleRows) {
const fieldError = validateAplnRow(row);
if (fieldError) return fieldError;
}
return null;
}
export function validateUniqueSeqNo(rows: ElaprAplnRow[]): string | null {
const seqSet = new Set<number>();
for (const row of getVisibleRows(rows)) {
if (row.atrzSeqNo == null) continue;
if (seqSet.has(row.atrzSeqNo)) {
return "동일 서식 내 결재순서번호가 중복됩니다.";
}
seqSet.add(row.atrzSeqNo);
}
return null;
}
@@ -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",
}
);
}

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