Compare commits

...
Author SHA1 Message Date
changwng c3711c2609 [as fr] 금액 없을 경우 메시지 표시 2026-10-23 17:59:12 +09:00
changwng 957fe4637e [as fr] 병렬결재 2차 검토완료 시 문서상태 SL 역전 fr 해결 2026-10-23 16:58:56 +09:00
changwng 4e9bca25ef [as fr] 리스트에서 뷰로 들어갈경우에 TanStack Query(React Query) 캐시 시스템과의 충돌 여부 및 웹한글기안기 등 인접 기능에 미치는 영향을 검토한 뒤 실질적인 해결 방안 추가2 2026-10-23 15:20:26 +09:00
changwng 23d62ab5e6 [as fr] 리스트에서 뷰로 들어갈경우에 TanStack Query(React Query) 캐시 시스템과의 충돌 여부 및 웹한글기안기 등 인접 기능에 미치는 영향을 검토한 뒤 실질적인 해결 방안 추가 2026-10-23 15:20:07 +09:00
changwng bedefd7e2e Merge branch 'main' into feature/1007_forkjoin
# Conflicts:
#	.vscode/launch.json
#	.vscode/settings.json
2026-10-23 14:06:26 +09:00
changwng 56688eb1a6 [as fr] 병합자 웹기안기 새창으로 SP, SF 참조 하도록 하기 2026-10-23 14:03:46 +09:00
mind-one005 ee0049722f Merge branch 'iipbs/sn/dev' 2026-10-22 15:01:40 +09:00
jcchoi 74934d4559 수정
Deploy Frontend / deploy (push) Successful in 4m0s
2026-10-22 14:28:31 +09:00
jcchoi 027f88da6c 대시보드 ㅁ이상치리포트 명칭 변경 2026-10-22 14:24:21 +09:00
changwng 175964b652 [as fr] 광역지자체 설치인가, 재원협의 반려 처리 restapi 추가및 알림처리 *액션 디스패처(executeAction)**와 **룩업 테이블(buttonActionHandlers)** 패턴 2026-10-22 11:54:59 +09:00
mind-one005 eddb1b24d6 sn: 지반침하 지도상세정보 엑셀다운로드 2026-10-22 10:19:52 +09:00
mind-one005 cf2cc11167 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-10-22 09:44:24 +09:00
mind-one005 9e71f529e7 sn: 통수위험 위험여부란 수정 외 2026-10-22 09:33:27 +09:00
changwng 4f4f5112b5 [as fr] 웹기안기에서 지자체는 SP, SF 탭을 볼수있는 권한이 SL에서 존재 한다. 2026-10-21 16:24:54 +09:00
changwng 6dc2965209 [as fr] 필수첨부 파일에 필수파일검토내용에 CRUD 처리 및 AI 요청시 같이 전송처리
Deploy Frontend / deploy (push) Successful in 4m5s
2026-10-21 16:14:39 +09:00
changwng 6d9db4389d [as fr] 필수첨부 파일에 필수파일검토내용에 CRUD 처리 및 AI 요청시 같이 전송처리 2026-10-21 16:13:26 +09:00
changwng b5eceadee8 [as fr] 한글기안기 변경이력 관리 추가 2026-10-21 15:46:10 +09:00
changwng b74f2820da [as fr] // 0-1. 지자체(ROLE_USER05, ROLE_USER04), 환경청(ROLE_USER03) 사용자인 경우: 병렬 Join 결재 시 MAIN 탭만 호출 2026-10-21 15:16:01 +09:00
jcchoi c69c1aa855 Merge branch 'iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 9m0s
2026-10-21 13:15:44 +09:00
changwng d0f2e41874 Merge branch 'main' into feature/1007_forkjoin 2026-10-21 11:43:24 +09:00
changwng 4be56402d8 [as fr] 2차 결재 진행시 버튼 2차결재시 검토의견 팝업 뜨지 못하도록 수정함 2026-10-21 11:42:30 +09:00
jcchoi 9f0e03c151 이상치 선별보고서 2026-10-21 11:31:50 +09:00
jcchoi 471a431af5 대시보드 진행상태 변경 2026-10-21 10:37:32 +09:00
jcchoi 26d8410649 Merge remote-tracking branch 'origin/iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 5m0s
2026-10-20 21:52:31 +09:00
jcchoi 8403ad556c 설치인가 버튼 이벤트 수정 2026-10-20 21:50:41 +09:00
rsone001 d26c15f857 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-10-20 17:49:42 +09:00
rsone001 86ed664e3e [input] - 오류 로직 수정 2026-10-20 17:49:09 +09:00
changwng 7df6f40bc4 [as fr] 체크리스트가 없으면 LOCAL rvwStpComCd 에 디폴트 입력 2026-10-20 17:34:28 +09:00
changwng 690724bc74 [as fr] 빌드 에러 수정 평가 다이얼 로그 쪽 2026-10-20 16:44:50 +09:00
changwng 08bdbe79b5 Merge branch 'main' into feature/1007_forkjoin
# Conflicts:
#	apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx
2026-10-20 16:30:04 +09:00
changwng 177f4653c8 [as fr]pm2 통합방법 2026-10-20 16:29:33 +09:00
changwng cc4acb3916 [as fr] NODE_TLS_REJECT_UNAUTHORIZED =0 추가 2026-10-20 15:56:48 +09:00
jcchoi a105f395dd 설치인가 프로세스 변경 2026-10-20 15:50:05 +09:00
changwng a06e4f46cf [as fr] doc 배포 문서및 빌드 에러 해결 방안 2026-10-20 15:39:49 +09:00
changwng 07ae3f7711 [as fr] 웹기안기 호출 팝업에 빌드및 lint 에러 해결 2026-10-20 15:34:24 +09:00
changwng 46c4634868 [as 2] as config로 pm2 로 시작 하기 위한 방법 2026-10-20 15:17:40 +09:00
changwng 0ce35d4c38 [as 2] port 3004번 포트로 수정 2026-10-20 14:25:52 +09:00
jcchoi a5ebc12580 검토의견 수정 오류 처리 2026-10-20 13:48:39 +09:00
jcchoi 270f6e366f Merge branch 'iipbs/st/dev' 2026-10-20 10:53:41 +09:00
jcchoi acf215c1bf 대시보드 이상치선별보고서 수정 2026-10-20 10:45:29 +09:00
jcchoi 8a98cd7d19 행정지원 검토의견 수정 2026-10-20 10:04:42 +09:00
jcchoi 3a5b48b608 통계 도움말 오류 수정 2026-10-20 10:04:26 +09:00
jcchoi d735ad7dd7 통계 파라미터 변경
Deploy Frontend / deploy (push) Successful in 1m57s
2026-10-19 21:28:01 +09:00
jcchoi 25dd0f1504 getVerifyResultList 파라미터 변경
Deploy Frontend / deploy (push) Successful in 2m2s
2026-10-19 19:17:01 +09:00
jcchoi 8e14c6cef5 Merge remote-tracking branch 'origin/iipbs/st/dev' 2026-10-19 19:07:14 +09:00
changwng f220904742 [as] 2차 결재 버튼 표시 부분 검증 2026-10-19 19:05:09 +09:00
jcchoi 9f7abe4413 필수첨부파일 위치 업데이트 2026-10-19 18:41:58 +09:00
rsone001 de730205af [ST] - 일반 검증 등록 및 수정 다이얼로그 css 수정 2026-10-19 17:50:49 +09:00
changwng dc74c07ef4 [as] 웹기안기의 첨부 파일 조정 확인 처리 2026-10-19 17:25:40 +09:00
kiminchan 36b11d6b00 맨홀 조사보고서 단독 교체 및 GIS 번호 기반 조회 수정 2026-10-19 17:01:37 +09:00
changwng 9a77e9c52a [as] 웹기안기의 화면 조정 2026-10-19 16:43:51 +09:00
rsone001 a873f0e0dc [ST] - api 표준화 누락 수정 2026-10-19 15:14:58 +09:00
changwng 42dc2dc027 [as]병렬결재에서 병합자 - 웹기안기 MAIN만 수정가능하게 하고 최종승인자의 경우에는 MAIN 탭만 보이도록 수정함 2026-10-19 14:59:52 +09:00
changwng 14803b2625 [as] 재원협의 병렬및 순차 결재시 버튼및 웹기안기 권한 1차 결재시 승인자까지 점검 완료 2026-10-19 14:23:36 +09:00
mind-one005 79f70d8b4b sn: 도시침수지도 geoserver style적용 2026-10-16 18:05:48 +09:00
mind-one005 7911cff96a sn: GIS편집,GIS관리, 통수위험 정보카드 X버튼 2026-10-16 17:36:31 +09:00
changwng 1a5180551e [as] 재원협의 병렬및 순차 결재시 버튼및 웹기안기 권한 제어 완료 2026-10-16 17:33:22 +09:00
mind-one005 fe501965bf sn: 조사관리>>조사사업 2026-10-16 16:51:47 +09:00
mind-one001 fb82cadc82 준설관리 텍스트 수정 2026-10-16 11:10:47 +09:00
mind-one001 f5b6effeb3 준설관리 관로 미표출로 인한 기능수정 2026-10-16 11:00:13 +09:00
mind-one001 63c2901ae3 feat(sn): allow dredging records without a mapped pipe 2026-10-16 10:09:44 +09:00
jcchoi 32e3765b52 초안 평가 단계 수정
Deploy Frontend / deploy (push) Successful in 1m53s
2026-10-15 19:56:54 +09:00
changwng e697881581 [as elapr] 재원업무 협의 2차 결재시 자동 검토자 지정 로직 추가 3 2026-10-15 17:02:26 +09:00
rsone001 4f62095e8e [ST] - fix: input 2-3 검증 cell 스타일 로직 수정 2026-10-15 16:03:40 +09:00
jcchoi 5234448ae5 기본계획 조회 전체 적용
Deploy Frontend / deploy (push) Successful in 1m40s
2026-10-15 15:53:38 +09:00
mind-one001 0356aef7b2 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-10-15 11:23:53 +09:00
mind-one001 f709dc99f6 chore: stop tracking local VS Code settings 2026-10-15 11:21:44 +09:00
jcchoi f284c67988 hwp 서버 도메인 연결 2026-10-15 10:43:15 +09:00
changwng dd4ee1dcb5 [as elapr] 화면에서 동적으로 결재자 숫자 변경및 체크 리스트 SP, SF 필수 로직으로 저장 변경 2026-10-14 18:00:37 +09:00
jcchoi 2d77813dab Merge remote-tracking branch 'origin/iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 4m31s
2026-10-14 17:36:08 +09:00
rsone001 d22a255143 [ST] - fix: type 오류 수정 2026-10-14 17:35:32 +09:00
jcchoi 6703bbeeec Merge remote-tracking branch 'origin/iipbs/st/dev' 2026-10-14 17:25:16 +09:00
rsone001 d444c591ec [ST] - flx: input 표준화에 맞춰 소스 수정 2026-10-14 17:05:45 +09:00
rsone001 f7ddb6cae0 Merge remote-tracking branch 'origin/feature/sein' into iipbs/st/dev 2026-10-14 15:39:21 +09:00
yangsein 7ffc02e887 refactor: 대시보드 통계처리현황 표준화 수정 2026-10-14 15:17:25 +09:00
yangsein cbacb27bb1 refactor: 행정동 갱신 표준화 수정 2026-10-14 15:16:40 +09:00
100JM 03a08a320a [ST] - fix: DB 표준화에 따른 소스 수정 2026-10-14 14:24:03 +09:00
changwng 6c71bccc27 [as elapr] 재원업무 협의 2차 결재시 자동 검토자 지정 로직 추가 2026-10-13 18:21:08 +09:00
mind-one005 728a66bac2 sn: apigateway.onasys.co.kr 2026-10-13 15:55:31 +09:00
mind-one005 c850b9ce1e sn: 해당 도시침수지도가 없습니다. 2026-10-13 15:48:24 +09:00
changwng 9a33663f1e [as elapr] 공공하수도 재원협의 복합사업(SF+SP) Fork-Join 전자결재 프론트엔드 병합자(ATRZ04) 결재 버튼 연동 및 순차결재 분리 개선 2026-10-13 15:25:42 +09:00
mind-one005 791bc20e8e sn: 지도 위치 정보가 없는 경우 404>>위치 정보가 없습니다. 2026-10-13 15:22:16 +09:00
changwng b088918cd9 [as elapr] 전자결재 forkjoin 통합 개발 화면 추가 브랜치 항목 2026-10-13 14:05:44 +09:00
jcchoi 45223be64a 포탈 수정
Deploy Frontend / deploy (push) Successful in 3m45s
2026-10-13 11:44:05 +09:00
jcchoi b22dfdfa4c 웹기안기 도메인 적용 2026-10-13 11:22:08 +09:00
jcchoi ce219f0e40 https 적용
Deploy Frontend / deploy (push) Successful in 4m5s
2026-10-13 10:53:39 +09:00
jcchoi a0f26d38b2 행정지원 리스트 디자인 cs 와 통일
Deploy Frontend / deploy (push) Successful in 3m42s
2026-10-12 19:19:13 +09:00
jcchoi 4efdcfe0fc 공통 테마 적용
Deploy Frontend / deploy (push) Successful in 7m55s
2026-10-12 18:47:26 +09:00
jcchoi 806ccf950f 행정지원 대시보드 테마 적용 2026-10-12 18:38:00 +09:00
changwng 3f2938df1c [as elapr] 전자결재 forkjoin 통합 개발 화면 2026-10-12 17:45:55 +09:00
jcchoi b96b78fa0b skin 및 레이아웃 작업 2026-10-12 17:22:37 +09:00
jcchoi f816bf005e 파일보안 간격 조절
Deploy Frontend / deploy (push) Successful in 9m15s
2026-10-12 11:54:55 +09:00
jcchoi 791ad6ce37 파일보안 줄바꿈 2026-10-12 11:50:50 +09:00
jcchoi 4bc802cee2 파일보완 내용 자동 줄빠굼 2026-10-12 10:55:19 +09:00
jcchoi 4662c8c896 선형 구조 수정 2026-10-12 10:50:09 +09:00
jcchoi 7058560a03 메뉴 배치 수정 2026-10-12 10:09:52 +09:00
jcchoi 79a5999ad5 콘텐츠의 선형 구조 2026-10-12 10:09:39 +09:00
jcchoi 1aeded5da2 페이지 타이틀 2026-10-12 10:09:16 +09:00
jcchoi e8d1d882c2 페이지 타이틀 수정 2026-10-12 10:01:22 +09:00
jcchoi 39dca810a5 css 오류 수정
Deploy Frontend / deploy (push) Successful in 7m50s
2026-10-11 17:58:09 +09:00
jcchoi 97f94688f1 Html 문법 오류
Deploy Frontend / deploy (push) Successful in 7m57s
2026-10-11 17:36:43 +09:00
jcchoi 0b502cbc99 접근성 체크 박스 (적절한 링크 텍스트)
Deploy Frontend / deploy (push) Successful in 7m28s
2026-10-11 17:09:37 +09:00
jcchoi a8eb8f9ab1 로고 alt 작업
Deploy Frontend / deploy (push) Successful in 7m43s
2026-10-11 16:16:14 +09:00
jcchoi 6471f4365a 접근성 2026-10-11 16:14:08 +09:00
jcchoi 48999f5699 웹 접근성 조치
Deploy Frontend / deploy (push) Successful in 9m6s
2026-10-11 15:32:56 +09:00
jcchoi 5ad64a596b 담당자 정보 caption 수정 2026-10-11 14:27:59 +09:00
jcchoi df60bd0b40 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend
Deploy Frontend / deploy (push) Successful in 7m13s
2026-10-11 13:27:16 +09:00
jcchoi 41e6f1cf48 Table Cpation 처리 2026-10-11 13:16:56 +09:00
jcchoi 0802ba191a DataTable Caption 추가 2026-10-11 12:58:04 +09:00
mind-one005 ad57ed241d Merge branch 'iipbs/sn/dev' 2026-10-08 13:29:37 +09:00
mind-one001 fed1383ced 시도별 노후관로현황 기본 조회기간을 당해연도로 변경 2026-10-08 10:30:51 +09:00
changwng c39d1f433a [common] vs code debug settings 2026-10-08 09:21:10 +09:00
jcchoi f17cb420e3 LNB GNB 탭 포커스 2026-10-07 21:42:43 +09:00
jcchoi c2396f093b 필수입력 안내 메세지
Deploy Frontend / deploy (push) Successful in 8m31s
2026-10-07 19:43:06 +09:00
jcchoi bdf65dcfd1 설치인가 사업구분 팝업 사이즈 조정 2026-10-07 19:42:42 +09:00
jcchoi 2aefbe17ee 웹접근성 2차 수정 2026-10-07 19:42:20 +09:00
mind-one001 91ff80a255 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-10-07 15:15:32 +09:00
mind-one005 5e43d69d8e sn: null 인경우, 공백으로 보이도록 2026-10-07 15:05:40 +09:00
mind-one001 7f10e20a41 전국 지도 보이게 줌 레벨 수정 2026-10-07 15:03:03 +09:00
jcchoi 4482b92ccd 메뉴간 탭이동
Deploy Frontend / deploy (push) Successful in 7m33s
2026-10-06 17:41:39 +09:00
jcchoi d3c39caefb 웹 접근성 조치 2026-10-06 14:32:26 +09:00
jcchoi 24c5dce24d 대체 텍스트 적용 2026-10-06 13:18:58 +09:00
mind-one005 9971e512c8 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-10-06 13:15:48 +09:00
mind-one005 0c77ed9bba sn: 도시침수예방사업 사업명 필수 2026-10-06 13:15:32 +09:00
kiminchan e759c96ade Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-10-06 13:06:20 +09:00
kiminchan f602b75268 페이지별 도움말 조회를 위한 메뉴 코드 전달 누락 수정 2026-10-06 13:06:01 +09:00
jcchoi d4d9eaf773 CM 로그인
Deploy Frontend / deploy (push) Successful in 5m32s
하단 메뉴 링크
2026-10-06 11:13:46 +09:00
jcchoi 68a800cdfc 메인페이지 탭이동
Deploy Frontend / deploy (push) Successful in 2m54s
2026-10-02 21:52:10 +09:00
jcchoi ff45d1a875 스킵 네비게이션 2026-10-02 21:51:56 +09:00
mind-one005 11d0378060 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-10-02 21:26:43 +09:00
jcchoi 49000c6140 FAQ 웹접근성(툴팁) 2026-10-02 21:25:44 +09:00
jcchoi 24bba484ca Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m49s
2026-10-02 19:43:52 +09:00
jcchoi c8d0d85937 Merge branch 'deploy' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into deploy 2026-10-02 19:42:49 +09:00
jcchoi c692f7c647 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend 2026-10-02 19:40:06 +09:00
jcchoi 080463458b 도움말 표시 2026-10-02 19:38:00 +09:00
jcchoi 08aaf4dd62 누락 리소스 추가 2026-10-02 19:29:13 +09:00
mind-one005 8ed4f959cb sn: 확률강우량 모달 추가 2026-10-02 18:12:03 +09:00
mind-one005 e5f7778861 sn: 확률강우량 모달 추가 2026-10-02 18:07:00 +09:00
chanii8 ad1e517472 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m56s
2026-10-02 17:55:30 +09:00
chanii8 17ed5eb46b Merge branch 'iipbs/as/dev' 2026-10-02 17:54:53 +09:00
chanii8 1a2faf02da [as] feat: 알림톡발송 페이지 구현 2026-10-02 17:54:25 +09:00
kiminchan 2d086a05b2 조사사업·준설 등록 필수 표시 및 숫자 입력 제한 보완 2026-10-02 17:11:04 +09:00
kiminchan 6ef4603a7c 지도 대상 선택 이동 시 지역·시설 검색조건 초기화 2026-10-02 16:51:47 +09:00
chanii8 d6522338aa [as] chore: 행정지원 숫자 폼 최대자릿수 제한 2026-10-02 16:37:26 +09:00
chanii8 df154a45b6 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m3s
2026-10-02 15:16:46 +09:00
chanii8 c5dbea334d Merge branch 'iipbs/as/dev' 2026-10-02 15:15:36 +09:00
chanii8 85f535eb5d [as] chore: 행정지원 숫자 폼 음수 입력 제한 2026-10-02 15:14:22 +09:00
chanii8 9334514c71 [as] chore: 행정지원 폼 사업기간 스키마 수정 2026-10-02 14:55:42 +09:00
chanii8 d438755616 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 7m37s
2026-10-02 13:12:44 +09:00
chanii8 54595fb7b6 Merge remote-tracking branch 'origin/iipbs/st/dev' 2026-10-02 13:07:41 +09:00
chanii8 4ee8d5a099 Merge branch 'deploy' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into deploy 2026-10-02 13:05:57 +09:00
chanii8 abe5fae4c4 Merge branch 'iipbs/as/dev' 2026-10-02 13:03:56 +09:00
chanii8 aeb48661b1 [as] chore: 보완 요청 사항 적용
- 사업기간 폼 포매팅
2026-10-02 13:03:24 +09:00
jcchoi 4bdf68c2b6 좌측메뉴 탭 이동 처리 2026-10-02 11:39:49 +09:00
chanii8 c5a0e378fd [as] feat: 보완 요청 사항 적용
- 실무협의서 내 보완요청 버튼 추가
- 대시보드 수정
2026-10-02 11:37:45 +09:00
jcchoi 538f656640 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Canceled after 6m27s
2026-10-01 22:25:22 +09:00
jcchoi a323c42acf 테이블에 scope="col" 및 캡션 추가
Deploy Frontend / deploy (push) Successful in 4m16s
2026-10-01 22:15:11 +09:00
jcchoi 095101ad2f 필수항목 tooltip 2026-10-01 22:14:28 +09:00
jcchoi 7ee5ebd506 글자 확대 축소 2026-10-01 20:26:45 +09:00
jcchoi f90ef6297d 상단메뉴 클릭 표준 2026-10-01 18:03:03 +09:00
chanii8 4167107a91 [as] feat: 파일 업로드 에러 로직 수정 2026-10-01 16:41:10 +09:00
kiminchan 90c1cb1f52 Merge branch 'iipbs/sn/dev' 2026-10-01 16:40:10 +09:00
mind-one001 8c614b84a7 merge(sn): preserve modular map entry with remote import cleanup 2026-10-01 16:22:02 +09:00
kiminchan 7d1a501ad8 관망도 페이지 import 병합 충돌 해소 2026-10-01 16:03:12 +09:00
mind-one001 48589274c8 merge(sn): integrate latest survey layers and upload guidance 2026-10-01 15:59:43 +09:00
kiminchan 83f7676db4 조사 레이어 표출 및 조사결과 업로드 안내 개선 2026-10-01 15:55:29 +09:00
mind-one001 c8b30810f6 style(sn): format integrated map modules 2026-10-01 15:54:43 +09:00
mind-one001 f71fa7d97a merge(sn): retain remote layer and flood updates after refactoring 2026-10-01 15:53:30 +09:00
mind-one001 4c7c4c00ab refactor(sn): modularize map and features and standardize file naming 2026-10-01 15:47:57 +09:00
chanii8 d2645a9c08 [as] feat: 파일 업로드 확장자 제한 정책 적용 2026-10-01 15:36:02 +09:00
mind-one005 ae341d66ba sn: 침수예측 오버레이 수정 2026-10-01 15:02:53 +09:00
chanii8 fce4def951 [as] feat: 행정지원 숫자 폼 소숫점 자리 제한 2026-10-01 14:26:22 +09:00
chanii8 ba0964f772 [as] feat: useTus > handleError 수정 2026-10-01 13:44:46 +09:00
kiminchan e346417c29 Merge branch 'iipbs/sn/dev' 2026-10-01 10:15:58 +09:00
kiminchan 5fcc184a88 노후관로 레이어를 DB 설정 기반으로 전환 2026-10-01 09:46:16 +09:00
mind-one005 f6d3f89b5b Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-10-01 00:26:53 +09:00
mind-one005 106e09a7a2 sn: 도시침수예방사업 지정일 수정 2026-10-01 00:26:36 +09:00
mind-one005 d1b4600392 sn: 도시침수예방사업 수정화면 사업순번 단계 수정 불가 2026-10-01 00:16:07 +09:00
kiminchan 4204dfa624 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-30 18:15:15 +09:00
kiminchan d7fd5be068 관로분석 및 통수위험 기본 조회조건 개선 2026-09-30 18:13:31 +09:00
mind-one005 735bb397ec sn: 침수예측현황 분석시각 조건 초기화 2026-09-30 18:09:20 +09:00
mind-one005 8b5a2b713c sn: 도시침수지도 자동선택 2026-09-30 17:53:50 +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
chanii8 7090f02a52 [as] feat: 보완 요청 사항 반영 (260930_1128) 2026-09-30 11:28:28 +09:00
chanii8 12152a6810 [as] chore: 파일보완 다이얼로그 컨텐츠 최대 높이 지정 2026-09-29 17:41:37 +09:00
chanii8 c0c2d3d1c9 Merge remote-tracking branch 'origin/iipbs/as/dev' 2026-09-29 17:32:18 +09:00
chanii8 858844e81a [as] chore: 설계자문 진행상태 모델 수정 2026-09-29 16:58:01 +09:00
chanii8 1ff2731121 Merge branch 'iipbs/cs/dev' 2026-09-29 15:37:15 +09:00
chanii8 70db7662bb [cs] chore: 모듈API관리 폼 수정 2026-09-29 15:36:37 +09:00
chanii8 bb57ff72e7 [cs] feat: 모듈API관리 "API URL" 검색 조건 추가 2026-09-29 15:19:42 +09:00
chanii8 8df79fd624 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m47s
2026-09-29 14:40:09 +09:00
chanii8 7946c08c72 Merge branch 'iipbs/as/dev' 2026-09-29 14:29:32 +09:00
chanii8 6f20ef7273 [as] feat: 보완 요청 사항 반영 (260929_1429) 2026-09-29 14:29:13 +09:00
mind-one005 657ae77dde Merge branch 'iipbs/sn/dev' 2026-09-29 14:03:36 +09:00
mind-one005 4ff80dd257 cm: 유역별 정보 수정 2026-09-29 11:38:47 +09:00
mind-one005 9bb1069d26 cm: 노후 하수관로 현황 수정 2026-09-29 11:17:52 +09:00
chanii8 730be25948 Merge remote-tracking branch 'origin/main' into deploy
Deploy Frontend / deploy (push) Successful in 5m4s
2026-09-28 17:17:08 +09:00
chanii8 78d8fea7f5 Merge branch 'iipbs/as/dev' 2026-09-28 16:44:05 +09:00
chanii8 22ed78c87b [as] feat: 행정지원 페이지 useSearchParamsState 적용 2026-09-28 16:37:20 +09:00
mind-one005 f5331d4c50 cm: 유역별 정보 div 수정 2026-09-28 16:32:16 +09:00
mind-one005 f32fe0ec5b cm: 유역별 정보 2026-09-28 16:07:52 +09:00
chanii8 3b914fbc2e [as] feat: 보완 요청 사항 반영 (20260928_1521) 2026-09-28 15:21:19 +09:00
kiminchan 533df0eb16 Merge branch 'iipbs/sn/dev' 2026-09-28 13:23:36 +09:00
chanii8 9c38165afb Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 7m25s
2026-09-28 11:24:12 +09:00
mind-one001 7b506320b4 서버페이징 변경 2026-09-28 11:22:22 +09:00
chanii8 2b7c497c3e Merge branch 'iipbs/as/dev' 2026-09-28 11:20:57 +09:00
chanii8 4dee1309eb [as] feat: 보완 요청 사항 반영 (260928_1120) 2026-09-28 11:20:31 +09:00
kiminchan f3f8909e45 Merge branch 'iipbs/sn/dev' 2026-09-28 10:00:24 +09:00
mind-one001 d45cd79422 테스트코드 삭제 2026-09-23 16:00:28 +09:00
mind-one001 7cbb58ff32 노후관로 조회 기능 보완 2026-09-23 14:58:14 +09:00
chanii8 9242e1fb47 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 12m36s
2026-09-23 14:44:23 +09:00
chanii8 f5ce74d0ee Merge remote-tracking branch 'origin/iipbs/as/dev' 2026-09-23 14:39:57 +09:00
chanii8 3114280e83 [as] chore: 알림이력 배지 크기 조정 2026-09-23 14:39:24 +09:00
chanii8 c7b8b36f4a Merge branch 'iipbs/common/dev' 2026-09-23 14:17:15 +09:00
chanii8 60ee194cc9 [common/components] design: NoticePopover 스타일 수정 2026-09-23 13:55:16 +09:00
mind-one001 4f50593eac Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-23 13:50:59 +09:00
mind-one001 8818dd21c6 관망도 레이어 전체켜기 기능 추가 2026-09-23 13:50:50 +09:00
chanii8 db8ee298d9 Merge branch 'iipbs/cs/dev' 2026-09-23 11:40:04 +09:00
chanii8 9eef99a435 [cs] chore: 게시판 댓글 사용 여부 적용 2026-09-23 11:39:32 +09:00
chanii8 3d027ed374 [common/features] chore: 게시판 댓글 사용 여부 적용 2026-09-23 11:37:16 +09:00
chanii8 271b5d445d [as] chore: 재원협의 버튼 수정
Deploy Frontend / deploy (push) Successful in 3m57s
2026-09-23 11:12:26 +09:00
chanii8 2457d14ae9 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 6m32s
2026-09-23 09:51:39 +09:00
chanii8 d70eb17a72 Merge branch 'iipbs/as/dev' 2026-09-23 09:50:30 +09:00
chanii8 ddd619babc [as] feat: 행정지원 문서 체크리스트 레이블 변경 2026-09-23 09:48:08 +09:00
chanii8 5d27ee830e [as] feat: 행정지원 문서 체크리스트 탭 노출 알고리즘 수정 2026-09-23 09:35:35 +09:00
mind-one005 b73b83a49f Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-23 09:08:05 +09:00
mind-one005 549debf8bf sn: 노후하수관로정비사업 검색조건 추가 2026-09-23 09:07:56 +09:00
mind-one005 212b9a43a9 Merge branch 'iipbs/wr/dev' 2026-09-22 20:49:40 +09:00
mind-one005 f092a23ee4 wr: font 크기 조정 2026-09-22 20:45:22 +09:00
chanii8 25c357a423 [as] chore: 보완요청 섹션 첨부파일 관련 코드 수정 2026-09-22 16:21:13 +09:00
chanii8 a77246a1db [as] fix: 타입 에러 2026-09-22 15:39:06 +09:00
chanii8 f00ce31648 [as] refactor: zod 스키마 .optional()-> .nullish() 변경 2026-09-22 15:12:28 +09:00
chanii8 dc57a491dd [as] chore: 재원협의 코드 수정 2026-09-22 15:02:36 +09:00
mind-one005 c9b7266959 wr: 재이용실적 수정 2026-09-22 11:24:32 +09:00
chanii8 0dd2111cb9 [as] refactor: 재원협의 폼 useWatch + Controller 방식 제거 2026-09-22 10:57:41 +09:00
mind-one005 c39b048398 Merge branch 'iipbs/wr/dev' 2026-09-22 10:25:49 +09:00
chanii8 87fbf15e2b [as] feat: 재원협의 수정 2026-09-22 10:23:10 +09:00
mind-one005 db1cfaa1a6 wr: label 위치 조정 2026-09-22 10:05:49 +09:00
mind-one005 905b750877 Merge branch 'iipbs/sn/dev' 2026-09-22 09:28:50 +09:00
mind-one005 be14fe2603 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-22 09:06:39 +09:00
chanii8 9739609de4 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m15s
2026-09-21 17:51:47 +09:00
mind-one005 d7f101ee30 wr: 시도,시군구 전체"0" 2건 조회현상 수정 2026-09-21 17:51:09 +09:00
chanii8 20f7d595a1 Merge branch 'iipbs/as/dev' 2026-09-21 17:50:59 +09:00
chanii8 e909dc81ad [as] chore: 재원협의 버튼 노출 조건 수정 2026-09-21 17:50:23 +09:00
chanii8 a510779ea1 [as] feat: AI 만족도 평가 기능 구현 2026-09-21 17:24:01 +09:00
chanii8 c799c199c5 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 8m28s
2026-09-21 16:58:49 +09:00
chanii8 0818e9ade2 Merge branch 'iipbs/as/dev' 2026-09-21 16:57:46 +09:00
chanii8 e60a8bed8b fix: 빌드 에러 2026-09-21 16:57:16 +09:00
chanii8 17fdcd7d1f fix: 빌드 에러 2026-09-21 16:56:46 +09:00
chanii8 6412e1a997 Merge branch 'iipbs/as/dev' 2026-09-21 16:36:58 +09:00
chanii8 1674598480 [as] design: RHFDevtool 스타일 수정 2026-09-21 16:36:28 +09:00
chanii8 23ebf36244 [as] feat: "@hookform/devtools" 추가 2026-09-21 16:04:19 +09:00
chanii8 342db06685 [as] refactor: 행정지원 문서 저장 payload 스키마 수정 2026-09-21 15:18:42 +09:00
mind-one001 35b9f813c4 로그삭제 2026-09-21 15:08:07 +09:00
mind-one005 aafb326662 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-21 14:46:02 +09:00
mind-one005 aa1b309b26 wr: 중수도 단위, 모달 조회 순서, 시설구분 조회 2026-09-21 14:45:50 +09:00
100JM 2c355525df [il] - DB 표준화로 인한 소스 수정 2026-09-21 13:55:37 +09:00
chanii8 bd20a499d3 [as] feat: AI 수동 검증 버튼 추가 2026-09-21 13:34:26 +09:00
chanii8 5fec5d5239 [as] feat: RAG 인덱싱 상태일 때 검토자 지정 버튼 분기 처리 2026-09-21 11:33:59 +09:00
chanii8 5b3aaa463d Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m27s
2026-09-21 11:03:48 +09:00
chanii8 a14b22b1ef Merge branch 'iipbs/as/dev' 2026-09-21 11:02:55 +09:00
chanii8 50aa549311 [as] feat: 공단 실무협의서 관리 버튼 노출 조건 수정 2026-09-21 11:02:22 +09:00
kiminchan bf0aef59cd Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs/sn/dev_kic 2026-09-21 10:06:26 +09:00
kiminchan b2ddcd0cf3 sn : 통수위험 결과 안내문에 위험 판정기준 및 상태 색상 표시 2026-09-21 09:55:55 +09:00
chanii8 947dba5934 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m43s
2026-09-18 17:38:37 +09:00
chanii8 572ef04bb4 Merge branch 'iipbs/as/dev' 2026-09-18 17:37:31 +09:00
chanii8 110d6c689c [as] feat: UploadedFileChip 컴포넌트 props 추가(onDownload) 2026-09-18 17:33:45 +09:00
chanii8 f40d6fd5e8 [as] feat: AttachmentDownloadLink 컴포넌트 구현 및 적용 2026-09-18 16:51:00 +09:00
mind-one001 49ace44c9b 테스트 파일 삭제 2026-09-18 16:35:21 +09:00
chanii8 cc3bc548c9 [as] feat: 재원협의 코드 수정 2026-09-18 16:27:51 +09:00
chanii8 4816a512bf Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m40s
2026-09-17 17:08:09 +09:00
chanii8 cc3d7ccd7b Merge remote-tracking branch 'origin/iipbs/as/dev' 2026-09-17 17:05:08 +09:00
chanii8 43bc7db7d5 [as] chore: useAplnRows 수정 2026-09-17 17:04:31 +09:00
chanii8 b0672aead5 Merge branch 'iipbs/as/dev' 2026-09-17 16:48:50 +09:00
chanii8 086d5a18e8 [as] chore: 재원협의 코드 수정 2026-09-17 16:48:13 +09:00
mind-one005 91576f401b sn: 침수예측오버레이 수정 2026-09-17 15:41:08 +09:00
mind-one005 febcd18eda Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-17 14:05:11 +09:00
mind-one005 8cc9f0d291 sn: 침수예측현황 침수위험도 수정 2026-09-17 14:04:51 +09:00
chanii8 41643563f8 [as] chore: 수정사항 반영 2026-09-17 10:55:51 +09:00
chanii8 b6b58eaeba feat: 재원협의&실무협의서 로직 사이클 2026-09-17 10:00:00 +09:00
chanii8 e8a5701e73 [as] feat: 웹기안기 다이얼로그 내 금액검증결과 섹션 구현 2026-09-16 17:37:44 +09:00
kiminchan a8f2c8f41a GIS 편집 신규 관로 미리보기 표시 보완 2026-09-16 17:32:02 +09:00
100JM 241222dcd1 [il] - 연계 db 표준화로 인한 소스 수정 2026-09-16 14:52:38 +09:00
chanii8 380575dae0 [as] feat: 실무협의서 검토 로직(RR, RK) 작업 2026-09-16 14:19:39 +09:00
mind-one001 4d685d5a67 Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs/sn/dev 2026-09-16 11:15:30 +09:00
mind-one001 96196229f4 fix(sn): align business form limits with standardized database 2026-09-16 11:15:18 +09:00
chanii8 5e54039028 [as] feat: 실무협의서 검토자 지정(EV) 로직 작업 2026-09-15 16:42:21 +09:00
mind-one005 94b2e84c10 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-15 16:28:23 +09:00
mind-one001 80d2230c9a 불필요 파일 삭제 2026-09-15 10:00:42 +09:00
chanii8 4fad163b14 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m4s
2026-09-14 17:56:23 +09:00
chanii8 4d20a68f93 Merge branch 'iipbs/as/dev' 2026-09-14 17:52:41 +09:00
chanii8 90a1d6fdff [as] feat: 웹기안기 '열기' 기능 추가(개발용) 2026-09-14 17:52:16 +09:00
chanii8 00e1cb707e [as] feat: 재원협의 환경청 조치결과 검토 의뢰(RA) 로직 작업 2026-09-14 17:35:00 +09:00
chanii8 6eeb774a21 [as] feat: 재원협의 지자체 조치완료(RB) 로직 작업 2026-09-14 13:36:23 +09:00
chanii8 6c4b8a52dc [as] chore: 보완요청 사항 적용 2026-09-14 10:16:16 +09:00
chanii8 a7e0d8f826 [as] feat: 첨부파일 컴포넌트 생성 및 적용 2026-09-11 17:23:45 +09:00
chanii8 d1ccddc1d3 [as] feat: 재원협의 기술검토 로직 작업 2026-09-11 17:02:09 +09:00
mind-one005 d5f0c3bc95 sn: 침수발생이력, 침수예측현황 팝업크기 변경 외 2026-09-11 11:01:55 +09:00
chanii8 da2191f365 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m43s
2026-09-11 10:38:12 +09:00
chanii8 056643bd96 Merge branch 'iipbs/as/dev' 2026-09-11 10:36:03 +09:00
chanii8 01d5ed297e [as] chore: 재원협의 상세화면 게이트 수정 2026-09-11 10:35:33 +09:00
chanii8 55ab2b8562 Merge remote-tracking branch 'origin/main' into deploy
Deploy Frontend / deploy (push) Successful in 3m33s
2026-09-10 17:53:02 +09:00
chanii8 dae5d0a9f5 Merge remote-tracking branch 'origin/iipbs/as/dev' 2026-09-10 17:52:46 +09:00
chanii8 41ad33310a [as] feat: 실무협의서 관리 다이얼로그 내 첨부파일 섹션 추가 2026-09-10 17:50:56 +09:00
chanii8 526392c370 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m39s
2026-09-10 15:44:08 +09:00
chanii8 b38ee9c7c7 Merge branch 'iipbs/as/dev' 2026-09-10 15:43:30 +09:00
chanii8 9f7f1db9b8 [as] feat: 기안기 저장 코드 수정 2026-09-10 14:50:09 +09:00
mind-one001 b7134cb3e4 노후관로현황 그리드 수정 2026-09-10 14:06:24 +09:00
mind-one001 ec78658e46 시도별 노후관로현황 그리드 수정 2026-09-10 14:01:58 +09:00
mind-one001 44361af392 그리드 css 통합 2026-09-10 13:51:42 +09:00
mind-one001 76130caa6d Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-10 11:30:58 +09:00
mind-one001 352a78a0c5 gis 업로드 오류 기능 수정 2026-09-10 11:30:52 +09:00
chanii8 6020c80a7a Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 10m6s
2026-09-09 18:09:33 +09:00
chanii8 a2a165a42b Merge branch 'iipbs/as/dev' 2026-09-09 18:08:35 +09:00
chanii8 0f27c1e5ce [as] chore: 웹기안기 다이얼로그 수정 2026-09-09 18:08:08 +09:00
chanii8 e944ad4369 [as] feat: 실무협의서 웹기안기에 렌더링 2026-09-09 17:38:40 +09:00
kiminchan 65bee9586d 도시침수지도 목록 이동 운영 보완 기록 2026-09-09 16:45:12 +09:00
mind-one001 31cda8579a Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-09 15:42:53 +09:00
mind-one001 1dc40b9efc 빈 레이어 트리로 지도만 재사용하는 화면에서는 패널 외곽과 제목도 표시하지 않는다. 2026-09-09 15:42:31 +09:00
mind-one005 6cb3d8ad65 Merge branch 'iipbs/wr/dev' into iipbs/sn/dev 2026-09-09 14:06:31 +09:00
LEEKYUHYUN 225e2732d1 사이드패널 패딩 조정 및 스크롤바 수정 2026-09-09 11:35:59 +09:00
chanii8 768ad001d2 [as] feat: 재원협의 쿼리 무효화 로직 수정 2026-09-09 10:30:29 +09:00
mind-one005 866d11988d Merge branch 'iipbs/sn/dev' 2026-09-09 09:27:53 +09:00
mind-one005 d82ae99b6a sn: 오류 수정 2026-09-09 09:09:09 +09:00
mind-one005 eb4f6f1b09 Merge branch 'iipbs/sn/dev' 2026-09-08 22:29:39 +09:00
mind-one005 23341b93c7 sn: 침수흔적심선/위선 화살표 연속 scroll 추가 2026-09-08 18:15:40 +09:00
chanii8 32b778f961 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m47s
2026-09-08 17:14:51 +09:00
mind-one005 b99f4904b0 sn: featureClickEventPopup에서 침수흔적도 관련 분리 2026-09-08 17:13:36 +09:00
chanii8 17b1c12286 Merge branch 'iipbs/st/dev' 2026-09-08 17:01:32 +09:00
chanii8 c79824e358 [as] feat: 보완요청 파일 첨부 로직 수정 2026-09-08 17:01:02 +09:00
mind-one005 f742feca4b sn: 상단 주석 외 침수흔적도 관련 수정 2026-09-08 15:12:40 +09:00
rsone001 de8971bea6 [ST] fix: 대시보드 통계 처리 현황 panel css 수정 2026-09-08 13:43:26 +09:00
mind-one005 569e003827 sn: 팝업 화면 여백 조정 2026-09-08 11:01:53 +09:00
chanii8 c47c130fc3 [as] feat: 파일보완 요청 기능 구현 2026-09-08 10:29:27 +09:00
mind-one005 2db5dd59af Merge branch 'iipbs/wr/dev' into iipbs/sn/dev 2026-09-07 18:15:04 +09:00
mind-one005 8a8504dca9 sn: 운영정보 버튼 위치 수정 2026-09-07 17:51:36 +09:00
LEEKYUHYUN 7e82c6a802 "디지털하수관로" UI 수정 2026-09-07 17:44:46 +09:00
chanii8 3946fda013 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m42s
2026-09-07 17:27:45 +09:00
chanii8 db30b45267 Merge branch 'deploy' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into deploy 2026-09-07 17:27:38 +09:00
chanii8 01377b6013 Merge remote-tracking branch 'origin/iipbs/st/dev' 2026-09-07 17:26:39 +09:00
chanii8 b38be38169 [as] feat: AI 자동 진행 단계 대기 상태 텍스트 추가 2026-09-07 17:25:40 +09:00
chanii8 b9ac4a1c18 [as] feat: 재원협의 서비스(fnrsCnsltnService) 수정 2026-09-07 17:05:59 +09:00
mind-one005 bdc6251474 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-07 17:01:46 +09:00
mind-one005 479cf5c4a0 sn: 다단header 묶음 처리 외 2026-09-07 16:56:35 +09:00
chanii8 dbea497cc5 [as] feat: 웹한글 기안기 데모 페이지 생성 2026-09-07 15:24:06 +09:00
chanii8 94af73a810 [as] docs: 실무협의 로직 개발 가이드 추가 2026-09-07 15:22:21 +09:00
rsone001 a7fd6a8e16 [ST] - fix: 취약점 점검 관련 조치 2026-09-07 14:26:00 +09:00
mind-one001 2b3502161c 시설물 상세창 깨짐 수정 2026-09-04 17:16:32 +09:00
rsone001 8039ab8281 [ST] - refactor: 2-5 맨홀 지표 변경된 테이블에 맞게 수정, 일반검증 항목 입력기간일 시 등록/수정 불가 2026-09-04 16:05:26 +09:00
mind-one001 0544a72011 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-04 15:27:18 +09:00
mind-one001 34ba09ce1b 준설관리 기능 보완 2026-09-04 15:24:17 +09:00
chanii8 2bfe653ac0 [as] feat: 'dcrag-to' API 구현 2026-09-04 15:18:32 +09:00
mind-one005 aa5a71ae68 sn: 침입수, RDII 제목란 조금 넓게 외 2026-09-04 14:52:11 +09:00
mind-one005 9067e645b3 sn: 정보검색 기본정보 구분란 화면이 줄었을 때, 디자인 외 2026-09-04 14:33:25 +09:00
mind-one005 10d3fc096a sn: 시도 전체일 때, 시군구 목록 비우기 2026-09-04 11:00:38 +09:00
mind-one005 7fd95ba6f9 sn: src/shared/util/util.ts 적용 외 2026-09-04 10:32:36 +09:00
kiminchan b4982e07b8 Merge branch 'iipbs/sn/dev' 2026-09-04 10:22:18 +09:00
jcchoi 6cc2a02df4 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 1m9s
2026-09-03 18:31:48 +09:00
mind-one005 e707637c88 수정 2026-09-03 17:10:11 +09:00
mind-one005 5da89e8ee7 sn: appendApiSearchParams() 함수 적용 2026-09-03 17:09:58 +09:00
mind-one005 f9ff279895 Merge branch 'iipbs/sn/dev' into iipbs/wr/dev 2026-09-02 18:18:01 +09:00
mind-one005 4cde9b67c1 sn: util.ts 추가 정리 2026-09-02 18:10:50 +09:00
chanii8 0f107ff29d Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 6m16s
2026-09-02 16:32:42 +09:00
chanii8 5e69a9b6f0 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m44s
2026-09-01 17:20:52 +09:00
chanii8 ec9311c76b Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m36s
2026-08-31 17:23:27 +09:00
chanii8 4c85a7d249 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m37s
2026-08-28 17:45:20 +09:00
chanii8 09d4c2d26e Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m6s
2026-08-28 17:13:45 +09:00
chanii8 b8dac5dc72 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m47s
2026-08-28 17:08:08 +09:00
chanii8 73cafc9786 Merge remote-tracking branch 'origin/main' into deploy
Deploy Frontend / deploy (push) Successful in 3m40s
2026-08-28 14:34:02 +09:00
chanii8 c276ae0c5e Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m2s
2026-08-27 17:25:06 +09:00
chanii8 b4ed061a53 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m40s
2026-08-27 11:36:49 +09:00
chanii8 ddaaa8fc92 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m55s
2026-08-26 17:14:14 +09:00
chanii8 3a253df58a Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 7m10s
2026-08-25 17:11:23 +09:00
chanii8 21debe2f77 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m22s
2026-08-25 15:45:10 +09:00
chanii8 0e4f662dfd Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m19s
2026-08-25 10:34:34 +09:00
chanii8 4a797808c1 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m1s
2026-08-24 17:18:45 +09:00
chanii8 3a81b822f2 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m54s
2026-08-21 17:29:09 +09:00
chanii8 f5a7efb324 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m2s
2026-08-20 13:12:39 +09:00
chanii8 abc3ca184d Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m49s
2026-08-18 18:03:01 +09:00
chanii8 267c022d58 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m36s
2026-08-13 17:52:39 +09:00
chanii8 0fc2844928 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m56s
2026-08-13 17:22:37 +09:00
chanii8 7f1fe42ace Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into deploy
Deploy Frontend / deploy (push) Successful in 3m36s
2026-08-11 17:46:50 +09:00
chanii8 c83a13d680 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m7s
2026-08-10 18:06:03 +09:00
chanii8 3d7dcb6d0e Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m17s
2026-08-07 17:03:11 +09:00
chanii8 a46f24d6d7 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m44s
2026-08-06 17:12:47 +09:00
chanii8 2e4f2428f0 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m41s
2026-08-06 16:50:33 +09:00
chanii8 706cebfeae Merge branch 'main' into deploy 2026-08-06 16:22:09 +09:00
chanii8 ee7f5e3a07 Merge branch 'main' into deploy 2026-08-06 13:42:33 +09:00
chanii8 3d0160f21f Merge branch 'main' into deploy 2026-08-05 17:23:37 +09:00
chanii8 0aa0126caf Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 5m43s
2026-08-03 17:47:39 +09:00
chanii8 5ecdd37187 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m52s
2026-08-03 17:28:33 +09:00
chanii8 20cc2ca248 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m48s
2026-08-03 11:45:43 +09:00
chanii8 5cfd7d65ec Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 7m45s
2026-07-31 18:00:11 +09:00
chanii8 3b8307b674 Merge branch 'main' into deploy 2026-07-30 11:37:16 +09:00
chanii8 2f800c960d Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 10m24s
2026-07-24 16:59:38 +09:00
chanii8 2728b4058b Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m8s
2026-07-24 10:57:06 +09:00
chanii8 b842905572 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m28s
2026-07-23 11:44:35 +09:00
chanii8 85fa9a8658 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into deploy
Deploy Frontend / deploy (push) Successful in 10m37s
2026-07-22 14:42:42 +09:00
chanii8 5ae56481c2 Merge remote-tracking branch 'origin/main' into deploy
Deploy Frontend / deploy (push) Successful in 7m23s
2026-07-22 11:44:50 +09:00
chanii8 8079d7d6af Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 7m20s
2026-07-21 11:51:55 +09:00
chanii8 7c8eef1715 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 6m51s
2026-07-20 18:44:05 +09:00
chanii8 f10b3179c2 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 10m25s
2026-07-20 17:11:46 +09:00
chanii8 2e0bb5aa84 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into deploy
Deploy Frontend / deploy (push) Successful in 10m10s
2026-07-20 13:52:10 +09:00
chanii8 7ae8d42731 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 9m2s
2026-07-16 18:39:59 +09:00
chanii8 a9e65af77b Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m25s
2026-07-13 11:14:34 +09:00
chanii8 d21ecc975f Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 2m14s
2026-07-09 17:54:09 +09:00
chanii8 452509281a Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 5m8s
2026-07-09 17:40:25 +09:00
chanii8 2e3602eee6 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 11m12s
2026-07-08 18:55:17 +09:00
jcchoi 7dfd535841 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 2m54s
2026-07-06 16:54:57 +09:00
jcchoi 39d928b5b1 Merge remote-tracking branch 'origin/iipbs/st/dev' into deploy 2026-07-06 16:51:29 +09:00
jcchoi 525695072a Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 2m56s
2026-07-05 11:07:27 +09:00
jcchoi 972a4ce789 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 5m30s
2026-07-02 18:34:53 +09:00
jcchoi 879aadc068 Merge remote-tracking branch 'origin/iipbs/st/dev' into deploy
Deploy Frontend / deploy (push) Successful in 3m2s
2026-07-02 18:29:55 +09:00
1302 changed files with 87919 additions and 46485 deletions
+3 -1
View File
@@ -1,6 +1,7 @@
# turbo
.turbo/
.vscode/**/*
**/.vscode/
*.code-workspace
# dependencies
node_modules/
@@ -27,6 +28,7 @@ settings.json
.npmrc
.cursor
.agent
CLAUDE.MD
apps/**/tsconfig.tsbuildinfo
packages/**/tsconfig.tsbuildinfo
+119 -11
View File
@@ -1,7 +1,5 @@
{
// IntelliSense를 사용하여 가능한 특성에 대해 알아보세요.
// 기존 특성에 대한 설명을 보려면 가리킵니다.
// 자세한 내용을 보려면 https://go.microsoft.com/fwlink/?linkid=830387을(를) 방문하세요.
"version": "0.2.0",
"configurations": [
{
@@ -14,13 +12,91 @@
"dev:as"
],
"skipFiles": [
"<node_internals>/**"
"<node_internals>/**",
"**/node_modules/**"
],
"resolveSourceMapLocations": [
"${workspaceFolder}/**",
"!**/node_modules/**"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"sourceMaps": true,
"env": {
"NODE_OPTIONS": "--inspect"
"NODE_OPTIONS": "--inspect --max-old-space-size=8192"
}
},
{
"type": "node-terminal",
"request": "launch",
"name": "dev:as 서버 컴포넌트 디버그 (node-terminal)",
"command": "pnpm --filter as dev",
"cwd": "${workspaceFolder}"
},
{
"type": "node-terminal",
"request": "launch",
"name": "dev:as 통합 디버그 (Server + Client)",
"command": "pnpm --filter as dev",
"cwd": "${workspaceFolder}",
"serverReadyAction": {
"action": "debugWithChrome",
"pattern": "- Local:\\s+(https?://.+)",
"uriFormat": "%s",
"webRoot": "${workspaceFolder}/apps/as"
}
},
{
"type": "node",
"request": "launch",
"name": "Playwright: 현재 활성화된 테스트 디버그",
"runtimeExecutable": "pnpm",
"runtimeArgs": [
"--filter",
"cs",
"exec",
"playwright",
"test",
"${relativeFile}"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"skipFiles": [
"<node_internals>/**",
"**/node_modules/**"
]
},
{
"type": "node",
"request": "launch",
"name": "Playwright: cs 로그인 테스트 디버그",
"runtimeExecutable": "pnpm",
"runtimeArgs": [
"--filter",
"cs",
"exec",
"playwright",
"test",
"tests/login.spec.ts"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"skipFiles": [
"<node_internals>/**",
"**/node_modules/**"
]
},
{
"type": "node-terminal",
"request": "launch",
"name": "dev:cs 통합 디버그 (Server + Client)",
"command": "pnpm --filter cs dev",
"cwd": "${workspaceFolder}",
"serverReadyAction": {
"action": "debugWithChrome",
"pattern": "- Local:\\s+(https?://.+)",
"uriFormat": "%s",
"webRoot": "${workspaceFolder}/apps/cs"
}
},
{
@@ -33,13 +109,18 @@
"dev:cs"
],
"skipFiles": [
"<node_internals>/**"
"<node_internals>/**",
"**/node_modules/**"
],
"resolveSourceMapLocations": [
"${workspaceFolder}/**",
"!**/node_modules/**"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"sourceMaps": true,
"env": {
"NODE_OPTIONS": "--inspect"
"NODE_OPTIONS": "--inspect --max-old-space-size=8192"
}
},
{
@@ -49,7 +130,8 @@
"port": 9229,
"restart": true,
"skipFiles": [
"<node_internals>/**"
"<node_internals>/**",
"**/node_modules/**"
],
"sourceMaps": true,
"resolveSourceMapLocations": [
@@ -63,7 +145,15 @@
"name": "dev:cs 클라이언트 디버그 (Chrome)",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}/apps/cs",
"sourceMaps": true
"skipFiles": [
"<node_internals>/**",
"**/node_modules/**"
],
"sourceMaps": true,
"resolveSourceMapLocations": [
"${workspaceFolder}/apps/cs/**",
"!**/node_modules/**"
]
},
{
"type": "node",
@@ -75,13 +165,31 @@
"dev:st"
],
"skipFiles": [
"<node_internals>/**"
"<node_internals>/**",
"**/node_modules/**"
],
"resolveSourceMapLocations": [
"${workspaceFolder}/**",
"!**/node_modules/**"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"sourceMaps": true,
"env": {
"NODE_OPTIONS": "--inspect"
"NODE_OPTIONS": "--inspect --max-old-space-size=8192"
}
},
{
"type": "node-terminal",
"request": "launch",
"name": "dev:st 통합 디버그 (Server + Client)",
"command": "pnpm --filter st dev",
"cwd": "${workspaceFolder}",
"serverReadyAction": {
"action": "debugWithChrome",
"pattern": "- Local:\\s+(https?://.+)",
"uriFormat": "%s",
"webRoot": "${workspaceFolder}/apps/st"
}
}
]
+4 -1
View File
@@ -1,5 +1,8 @@
{
"chat.tools.terminal.autoApprove": {
"pnpm": true
}
},
"playwright.configs": [
"apps/cs/playwright.config.ts"
]
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { Providers } from "@/app/providers";
import "@/app/styles/globals.css";
export const metadata: Metadata = {
title: "행정지원시스템",
title: "유역하수도통합플랫폼 Hasuon",
description: "행정지원시스템",
};
+1 -1
View File
@@ -1,7 +1,7 @@
import "@/app/styles/globals.css";
export const metadata = {
title: "디지털 행정지원 시스템",
title: "유역하수도통합플랫폼 Hasuon",
};
export default function AuthLayout({ children }: { children: React.ReactNode }) {
@@ -1,3 +1,11 @@
import { Suspense } from "react";
import { DesignListPage } from "@/pages/design-cnstn";
export default DesignListPage;
export default function Page() {
return (
<Suspense>
<DesignListPage />
</Suspense>
);
}
@@ -1,3 +1,11 @@
import { Suspense } from "react";
import { FnrsListPage } from "@/pages/fnrs-cnsltn";
export default FnrsListPage;
export default function Page() {
return (
<Suspense>
<FnrsListPage />
</Suspense>
);
}
@@ -1,3 +1,11 @@
import { Suspense } from "react";
import { InstlListPage } from "@/pages/instl-sncn";
export default InstlListPage;
export default function Page() {
return (
<Suspense>
<InstlListPage />
</Suspense>
);
}
@@ -0,0 +1,3 @@
import { NtitlkSndngPage } from "@/pages/ntitlk-sndng";
export default NtitlkSndngPage;
@@ -1,3 +1,11 @@
import { Suspense } from "react";
import { SewerBscPlanListPage } from "@/pages/sewer-bsc-plan";
export default SewerBscPlanListPage;
export default function Page() {
return (
<Suspense>
<SewerBscPlanListPage />
</Suspense>
);
}
+1 -1
View File
@@ -6,7 +6,7 @@ import { Providers } from "@/app/providers";
import "@/app/styles/globals.css";
export const metadata: Metadata = {
title: "디지털 행정지원 시스템",
title: "유역하수도통합플랫폼 Hasuon",
description: "디지털 행정지원 시스템",
};
+1 -1
View File
@@ -4,7 +4,7 @@ import Image from "next/image";
import "@/app/styles/globals.css";
export const metadata: Metadata = {
title: "디지털 행정지원 시스템",
title: "유역하수도통합플랫폼 Hasuon",
description: "페이지가 존재하지 않습니다. 관리자에게 문의하여 주십시오.",
};
@@ -0,0 +1,203 @@
# PM2 배포 및 프로세스 관리 가이드
본 문서는 ONA 프론트엔드 모노레포 프로젝트(`frontend_as`)에서 **PM2를 활용한 애플리케이션 배포, 실행, 중지 및 모니터링 명령**을 체계적으로 정리한 가이드입니다.
---
pnpm build:common && pnpm build:as && pm2 reload as2-app
pnpm --filter=as... build
pnpm --filter=as... build && pm2 reload as2-app
## 1. 개요 및 아키텍처
- **설정 파일**: 프로젝트 루트의 `ecosystem.config.js`
- **배포 워크플로우**: `.gitea/workflows/deploy.yml` (Gitea Actions 자동 배포)
- **실행 모드**: `cluster` (2개 인스턴스로 로드밸런싱 및 무중단 롤링 배포 지원)
- **운영 대상**:
- `as2-app` (또는 `as-app`, `cs-app`, `st-app`, `cm-app` 등)
---
## 2. CI/CD 자동 배포 흐름
Gitea 저장소의 `deploy` 브랜치로 푸시될 때 원격 서버(192.168.0.4)에서 다음 스크립트가 순차 실행됩니다:
```bash
cd ~/frontend
git restore . # 변경된 로컬 파일 원복
git clean -fd # 추적되지 않는 불필요한 파일/디렉터리 정리
git pull origin deploy # 최신 deploy 브랜치 코드 반영
pnpm install # 의존성 패키지 설치
pnpm turbo build # 프로덕션 빌드 (.next 산출물 생성)
pm2 reload all # 무중단 서비스 리로드 (클러스터 롤링 재시작)
```
---
## 3. 수동 배포 (Manual Deploy) 명령어
서버 터미널에 직접 접속하여 수동으로 최신 코드를 배포할 때 사용하는 명령어입니다.
### 3.1 전체 애플리케이션 수동 배포
```bash
# 1. 프로젝트 루트 이동
cd ~/frontend
# 2. 최신 코드 가져오기
git pull origin deploy
# 3. 의존성 설치 및 전체 빌드
pnpm install
pnpm turbo build
# 4. PM2 전체 무중단 리로드
pm2 reload all
```
### 3.2 `as` (행정지원) 앱만 단독 빌드 및 배포
```bash
cd ~/frontend
# as 앱만 타겟 빌드
pnpm --filter as build
# as2-app 프로세스만 무중단 리로드
pm2 reload as2-app
```
---
## 4. PM2 실행 및 프로세스 등록 (Start & Save)
최초 서버 환경 구성 시 또는 프로세스 목록에서 삭제된 후 새롭게 등록할 때 실행합니다.
```bash
# 1. 로그 디렉터리 사전 생성 (필수)
mkdir -p apps/as/logs
# 2. ecosystem.config.js 기반 전체 앱 최초 실행
pm2 start ecosystem.config.js
# 3. 특정 앱(as2-app)만 지정하여 최초 실행
pm2 start ecosystem.config.js --only as2-app
# 4. 서버 재부팅 시에도 PM2 프로세스가 자동 복구되도록 현재 상태 저장 (필수!)
pm2 save
# 5. OS 부팅 시 PM2 데몬 자동 등록 (최초 1회 설정)
pm2 startup
```
---
## 5. 중지, 재시작 및 삭제 명령어
### 5.1 재시작 (Restart vs Reload)
> **TIP: 운영 환경에서는 항상 `reload`를 권장합니다.**
> - `reload`: 0-second downtime(무중단). 클러스터의 인스턴스를 하나씩 순차적으로 재시작하여 사용자 요청이 끊기지 않음.
> - `restart`: 모든 인스턴스를 일시에 강제 종료 후 다시 띄우므로 짧은 다운타임 발생.
```bash
# [권장] 무중단 재로드 (운영 배포 시)
pm2 reload as2-app
pm2 reload all
# 강제 재시작 (설정 변경이 반영되지 않거나 프로세스가 비정상일 때)
pm2 restart as2-app
pm2 restart all
```
### 5.2 프로세스 중지 (Stop)
프로세스를 메모리에서 내리고 실행을 일시 중단합니다 (PM2 목록에는 유지됨).
```bash
# 특정 앱 중지
pm2 stop as2-app
# 전체 앱 중지
pm2 stop all
```
### 5.3 프로세스 목록에서 제거 (Delete)
PM2 관리 대상 목록에서 프로세스를 완전히 삭제합니다.
```bash
# 특정 앱 삭제
pm2 delete as2-app
# 전체 앱 삭제
pm2 delete all
# 삭제 상태를 영구 저장할 경우
pm2 save
```
---
## 6. 상태 모니터링 및 로그 확인
### 6.1 프로세스 상태 확인
```bash
# 간략한 프로세스 상태 표 확인 (online, stopped, cpu, memory 등)
pm2 status
# 또는
pm2 list
# 특정 앱 상세 정보 확인 (환경변수, 파일 경로, 재시작 횟수 등)
pm2 show as2-app
```
### 6.2 실시간 로그 스트리밍 (Logs)
```bash
# 특정 앱 실시간 로그 확인 (표준 출력 + 에러 출력)
pm2 logs as2-app
# 최근 100줄 출력 후 실시간 스트리밍
pm2 logs as2-app --lines 100
# 에러 로그만 확인
pm2 logs as2-app --err
# 전체 프로세스 로그 확인
pm2 logs
# 이전 로그 비우기(Flush)
pm2 flush
```
### 6.3 터미널 대시보드 모니터링 (Monit)
CPU 및 메모리 실시간 사용량을 터미널 대시보드로 확인합니다.
```bash
pm2 monit
```
---
## 7. 주요 트러블슈팅 (FAQ)
### Q1. `EADDRINUSE: address already in use :::3004` 포트 충돌 발생
- **원인**: 로컬 개발 서버(`pnpm --filter as dev`) 또는 이전 Node 프로세스가 3004번 포트를 계속 점유 중인 상태입니다.
- **해결**:
```bash
# 3004 포트 사용 중인 PID 확인
lsof -i :3004
# 해당 프로세스 종료
kill -9 <PID>
# PM2 리로드
pm2 reload as2-app
```
### Q2. `Could not find a production build in the '.next' directory`
- **원인**: Next.js 프로덕션 실행 전에 빌드 산출물(`.next`)이 생성되지 않았습니다.
- **해결**:
```bash
pnpm --filter as build
pm2 reload as2-app
```
### Q3. `logs` 디렉터리 관련 에러 발생
- **원인**: `ecosystem.config.js`의 `cwd: './apps/as'` 설정에 따라 `./logs` 경로가 없을 경우 발생할 수 있습니다.
- **해결**:
```bash
mkdir -p apps/as/logs
```
@@ -0,0 +1,181 @@
# Next.js 프로덕션 빌드 에러 원인 분석 및 조치 보고서
본 문서는 `apps/as` 프로덕션 빌드(`pnpm build:as` / `next build`) 과정에서 발생한 ESLint Hook 규칙 위반 및 TypeScript 컴파일 에러의 원인과 이를 해결한 조치 내역을 정리한 문서입니다.
---
## 1. 개요 및 장애 상황
- **대상 모듈**: `apps/as` (행정지원 프론트엔드 애플리케이션)
- **실행 명령**: `pnpm build:as` (또는 `pnpm --filter as build`)
- **발생 시점**: 2026-10-20
- **장애 영향**: Next.js 프로덕션 최적화 빌드 실패로 인해 배포 산출물(`.next`) 생성이 중단됨
### 빌드 실패 로그 요약
```text
as:build: Failed to compile.
as:build: ./src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx
as:build: 159:35 Error: React Hook "useStore" is called conditionally. React Hooks must be called in the exact same order in every component render. react-hooks/rules-of-hooks
./src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx:164:5
Type error: Type 'boolean' has no properties in common with type '{ enabled?: boolean | undefined; structuralSharing?: boolean | undefined; }'.
./src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx:215:29
Type error: Property 'atrzSeq' does not exist on type 'ElaprDocPrgrsClientResponseDto'. Did you mean 'atrzSeqNo'?
```
---
## 2. 근본 원인 분석 (Root Cause Analysis)
### 2.1 React Hook 규칙 위반 (`react-hooks/rules-of-hooks`)
- **위치**: `apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx` (158~159라인)
- **원인 코드**:
```tsx
const storeContext = useContext(StoreContext);
const userInfo = storeContext ? useStore(storeContext.userStore, (state) => state.userInfo) : null;
```
- **문제점**:
- React Hooks는 조건문, 반복문, 삼항 연산자 내부에서 호출될 수 없으며, 모든 렌더링마다 항상 동일한 순서로 호출되어야 합니다.
- 삼항 연산자 내에서 `useStore`를 호출하면서 `react-hooks/rules-of-hooks` 에러가 발생하여 빌드가 즉시 차단되었습니다.
- 또한 프로젝트 전역에서 제공하는 표준 훅인 `useUserStore`(`@/shared/lib`)가 이미 존재함에도 불구하고 이를 사용하지 않고 수동으로 컨텍스트를 조회하여 분기 처리하고 있었습니다.
### 2.2 `useElaprBranchesByDocNo` 훅 옵션 매개변수 타입 불일치
- **원인 코드**:
```tsx
const { data: elaprBranchList = [] } = useElaprBranchesByDocNo(
fnrsCnsltnDocNo,
open && Boolean(isForkJoin), // boolean 직접 전달
);
```
- **문제점**:
- `useElaprBranchesByDocNo`의 시그니처는 `(elaprDocNo, options?: { enabled?: boolean; structuralSharing?: boolean })`로 선언되어 있어, `boolean` 대신 객체 형태(`{ enabled: ... }`)로 전달해야 합니다.
### 2.3 전자결재 결재선 DTO 필드명 불일치
- **원인 코드**:
```tsx
if (currentUserPrgrs?.atrzSeq && Number(currentUserPrgrs.atrzSeq) > 1) return true;
```
- **문제점**:
- `ElaprDocPrgrsClientResponseDto` 인터페이스(`src/shared/model/pbadms-sprt.ts`)에 선언된 정식 결재 순번 필드명은 `atrzSeqNo?: number`입니다.
- 기존 코드가 `atrzSeq`로 접근하여 TypeScript 컴파일 에러가 발생했습니다.
### 2.4 `PwrkCnsltnDocEvlDialog`의 필수 prop `onSaved` 누락
- **원인 코드**:
```tsx
<PwrkCnsltnDocEvlDialog
open={isEvlDialogOpen}
pwrkCnsltnDocNo={completedDraft.pwrkCnsltnDocNo}
onClose={() => setIsEvlDialogOpen(false)}
/>
```
- **문제점**:
- `PwrkCnsltnDocEvlDialogProps`에 `onSaved: () => void;`가 필수로 선언되어 있었으나, 호출부에서 `onSaved` 콜백을 넘기지 않아 `Property 'onSaved' is missing` 컴파일 에러가 발생했습니다.
---
## 3. 조치 내역
### 3.1 컴포넌트 코드 수정 (`pwrk-cnsltn-doc-mng-dialog.tsx`)
1. **표준 `useUserStore` 훅 적용**:
- `useStore`, `StoreContext`, `useContext`를 제거하고, `@/shared/lib`의 `useUserStore`를 무조건부로 호출하도록 변경.
2. **`useElaprBranchesByDocNo` 옵션 객체화**:
- `{ enabled: open && Boolean(isForkJoin) }` 형태로 수정.
3. **결재 순번 속성 방어적 매핑**:
- `atrzSeqNo`를 우선 참조하고 레거시/모의 데이터의 `atrzSeq`도 호환되도록 fallback 처리.
4. **`PwrkCnsltnDocEvlDialog`의 `onSaved` prop 연결 및 선택적(Optional) 처리**:
- `pwrk-cnsltn-doc-evl-dialog.tsx`: `onSaved?: () => void;`로 변경하고 `onSaved?.()` 호출로 방어.
- `pwrk-cnsltn-doc-mng-dialog.tsx`: `<PwrkCnsltnDocEvlDialog ... onSaved={() => setIsEvlDialogOpen(false)} />` 콜백 전달.
#### 핵심 변경 사항 (Diff)
```diff
--- a/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx
+++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx
@@ -1,14 +1,11 @@
"use client";
-import { useContext, useEffect, useMemo, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
import dynamic from "next/dynamic";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { PanelRightClose, PanelRightOpen } from "lucide-react";
-import { useStore } from "zustand";
-
-import { getDefaultFileKey, useAlertStore, useTus } from "@/shared/lib";
-import { StoreContext } from "@/shared/providers";
+import { getDefaultFileKey, useAlertStore, useTus, useUserStore } from "@/shared/lib";
@@ -155,8 +152,7 @@ function PwrkCnsltnDocMngDialog({
const queryClient = useQueryClient();
const alert = useAlertStore((state) => state.setMessage);
- const storeContext = useContext(StoreContext);
- const userInfo = storeContext ? useStore(storeContext.userStore, (state) => state.userInfo) : null;
+ const userInfo = useUserStore((state) => state.userInfo);
@@ -165,7 +161,7 @@ function PwrkCnsltnDocMngDialog({
const { data: elaprBranchList = [] } = useElaprBranchesByDocNo(
fnrsCnsltnDocNo,
- open && Boolean(isForkJoin),
+ { enabled: open && Boolean(isForkJoin) },
);
@@ -216,11 +212,19 @@ function PwrkCnsltnDocMngDialog({
if (effectiveBranchSeCd === "TOTAL_AGG") {
if (effectiveAtrzSeList.includes("ATRZ04")) return false;
if (effectiveAtrzSeList.some((cd) => APPROVER_ATRZ_SE_COM_CDS.has(cd))) return true;
- if (currentUserPrgrs?.atrzSeq && Number(currentUserPrgrs.atrzSeq) > 1) return true;
+ const userSeq = currentUserPrgrs?.atrzSeqNo ?? (currentUserPrgrs as Record<string, unknown> | undefined)?.atrzSeq;
+ if (userSeq && Number(userSeq) > 1) return true;
if (effectiveAtrzSeList.length > 0 && !effectiveAtrzSeList.includes("ATRZ04")) return true;
}
return false;
```
### 3.2 단위 테스트 모킹 보완 (`pwrk-cnsltn-doc-mng-dialog.test.tsx`)
- 단위 테스트 환경에서 `StoreProvider` 미감싸임 에러(`useUserStore must be used within StoreProvider`)가 발생하지 않도록 `@/shared/lib` 모킹 객체에 `useUserStore`를 추가.
```tsx
let mockUserInfo: any = null;
vi.mock("@/shared/lib", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/shared/lib")>();
return {
...actual,
getDefaultFileKey: () => "mock-key",
useAlertStore: () => vi.fn(),
useUserStore: (selector: (state: { userInfo: any }) => any) =>
selector ? selector({ userInfo: mockUserInfo }) : { userInfo: mockUserInfo },
useTus: () => ({
uploadFileAsync: vi.fn(),
isUploading: {},
uploads: {},
}),
};
});
```
---
## 4. 검증 결과
| 검증 단계 | 명령어 | 결과 | 비고 |
|---|---|---|---|
| **1. 정적 타입 검사** | `pnpm --filter as exec tsc --noEmit` | **성공 (Exit 0)** | 컴파일 에러 0건 |
| **2. ESLint 린트 검사** | `pnpm --filter as lint` | **성공 (Exit 0)** | Hook 위반 에러 해결 |
| **3. 단위 테스트 (Vitest)** | `pnpm --filter as exec vitest run` | **전체 통과 (73/73 Tests)** | 14개 테스트 파일 100% Pass |
| **4. 프로덕션 빌드** | `pnpm build:as` | **성공 (Exit 0)** | 22/22 정적 페이지 최적화 완료 |
---
## 5. 재발 방지 표준 가이드
1. **React Hook 호출 규칙 준수**:
- `useUserStore`, `useQuery`, `useStore` 등 모든 React Hook은 반드시 컴포넌트 함수의 최상단(Top-level)에서 무조건부로 호출해야 합니다.
- 사용자 정보가 필요할 경우 개별 컨텍스트 접근 대신 표준 훅인 `useUserStore((state) => state.userInfo)`를 사용합니다.
2. **React Query 커스텀 훅 매개변수 규칙**:
- 조건부 쿼리 실행이 필요한 경우 boolean 값을 단독으로 넘기지 않고 `{ enabled: boolean }` 형태의 옵션 객체 인터페이스를 유지합니다.
3. **엔티티 DTO 필드명 엄격 확인**:
- 전자결재 결재선 DTO는 백엔드 명세에 따라 `atrzSeqNo`를 표준으로 사용하므로 레거시 명칭(`atrzSeq`)을 사용하지 않도록 타입을 확인합니다.
@@ -0,0 +1,160 @@
# 개발(Dev) 및 운영(Production) 실행 가이드 & 환경변수 관리
본 문서는 `apps/as` 애플리케이션의 **개발(Dev) 모드** 및 **운영(Production) 모드** 실행 방법과 **환경변수(.env) 변경 시의 재빌드 규칙**을 상세히 정리한 가이드입니다.
---
## 1. Dev 모드 vs Production 모드 비교
| 구분 | 개발 모드 (Dev) | 운영 모드 (Production) |
|---|---|---|
| **실행 목적** | 로컬 개발, 디버깅, 실시간 코드 수정 | 실서버 운영, 성능 최적화, 안정성 보장 |
| **사전 빌드 필요 여부** | **불필요** (즉시 실행) | **필수** (`.next` 번들 생성 선행) |
| **코드 반영 방식** | 핫 리로딩(HMR, 저장 즉시 반영) | 빌드 시점의 번들링 코드 고정 서빙 |
| **참조 환경변수** | `.env.local` > `.env.development` | `.env.local` > `.env.production` |
| **프로세스 관리** | Node 단일 프로세스 (터미널) | **PM2 Cluster 모드** (2개 인스턴스 무중단) |
| **서비스 포트** | 기본 `3004` 포트 | 기본 `3004` 포트 |
---
## 2. 환경변수(.env) 변경 시 재빌드 필수 여부
### 2.1 결론: `.env.production` 변경 시 반드시 재빌드 필요
Next.js 환경에서는 `.env.production`을 수정했을 때 단순히 PM2 재시작(`pm2 reload` / `pm2 restart`)만으로는 변경 사항이 브라우저에 반영되지 않으며, **반드시 빌드(`next build`)를 다시 수행해야 합니다.**
### 2.2 이유: Next.js의 환경변수 인라이닝(Inlining) 원리
1. **`NEXT_PUBLIC_*` 변수의 빌드타임 주입**:
- `NEXT_PUBLIC_SERVER_API_URL`, `NEXT_PUBLIC_PROXY_HOST`, `NEXT_PUBLIC_WEBHYP_CTRL_URL` 등 브라우저에서 참조하는 변수들은 `next build` 시점에 생성되는 클라이언트 JavaScript 파일(`.next/static/...`) 안에 **문자열 값으로 직접 하드코딩(Inlining)**됩니다.
- 따라서 코드를 다시 빌드하지 않으면 예전 주소나 설정값이 그대로 유지됩니다.
2. **정적 페이지(SSG) 생성 시점의 값 고정**:
- 빌드 시 정적 생성된 페이지(`Generating static pages (22/22)`)는 빌드 당시 읽어들인 환경변수 값을 기준으로 생성되므로, 런타임에 환경변수를 바꿔도 페이지가 갱신되지 않습니다.
### 2.3 환경변수 우선순위 주의사항
Next.js의 환경변수 로딩 우선순위는 다음과 같습니다:
```text
[운영 환경: NODE_ENV=production]
1순위: .env.production.local
2순위: .env.local (NODE_ENV=test 제외 항상 최우선)
3순위: .env.production
4순위: .env
```
> [!WARNING]
> 실서버 환경에 `.env.local` 파일이 존재할 경우 `.env.production`보다 우선 적용됩니다. `.env.production`을 수정했는데도 반영되지 않는다면 같은 경로에 `.env.local`이 존재하는지 확인하십시오.
---
## 3. Dev (개발 모드) 실행 방법
사전 빌드 과정 없이 소스 코드를 실시간으로 수정하며 개발할 때 사용합니다.
### 3.1 프로젝트 루트 디렉터리에서 실행
```bash
# 루트 디렉터리: ~/frontend_as
pnpm dev:as
# 또는 pnpm filter 플래그 사용
pnpm --filter as dev
```
### 3.2 `apps/as` 디렉터리 내부에서 실행
```bash
cd ~/frontend_as/apps/as
pnpm dev
```
- **접속 주소**: [http://localhost:3004](http://localhost:3004)
- **종료**: 터미널에서 `Ctrl + C`
---
## 4. Production (운영 모드) 실행 방법
반드시 **빌드 산출물(`.next`)이 최신 상태로 생성되어 있어야 정상 실행**됩니다.
### 4.1 1단계: 프로덕션 빌드 (필수)
모노레포 전체를 빌드할 필요 없이 `as` 앱만 단독으로 빌드할 수 있습니다:
```bash
# 프로젝트 루트 디렉터리에서 실행 (약 20~30초 소요)
pnpm build:as
# 또는
pnpm --filter as build
```
---
### 4.2 2단계: 서버 프로세스 실행
#### 방법 A: 터미널에서 직접 실행 (로컬 프로덕션 점검용)
포그라운드 터미널에서 프로덕션 번들의 빌드 결과와 동작을 직접 테스트할 때 사용합니다:
```bash
# 루트 디렉터리에서 실행
pnpm --filter as start
# 또는 apps/as 디렉터리에서 실행
cd apps/as
pnpm start
```
---
#### 방법 B: PM2 프로세스 매니저로 실행 (실서버 운영 표준)
실제 운영 서버에서는 `ecosystem.config.js` 설정을 통해 무중단 클러스터(2개 인스턴스)로 실행합니다.
```bash
# 1. 프로세스가 아직 등록되지 않은 경우 (최초 기동)
pm2 start ecosystem.config.js --only as2-app
# 2. 이미 실행 중인 경우 (무중단 롤링 리로드 - 권장)
# 순차적으로 인스턴스를 재시작하므로 사용자 요청 끊김이 없습니다.
pm2 reload as2-app
# 3. 프로세스 완전 재시작이 필요한 경우
pm2 restart as2-app
```
---
## 5. PM2 운영 및 모니터링 치트시트
```bash
# 1. 전체 프로세스 상태 확인 (상태, 메모리, CPU 점유율 등)
pm2 status
# 또는
pm2 list
# 2. as2-app 실시간 로그 모니터링
pm2 logs as2-app
# 3. 최근 로그 100줄 출력
pm2 logs as2-app --lines 100
# 4. 프로세스 일시 중지 및 삭제
pm2 stop as2-app # 프로세스 일시 중지
pm2 delete as2-app # PM2 목록에서 제거
# 5. 서버 재부팅 시 자동 실행 등록
pm2 save # 현재 실행 중인 프로세스 목록 저장
pm2 startup # OS 부팅 스크립트 생성
```
---
## 6. 실무 배포 요약 워크플로우
`.env.production` 수정 또는 프론트엔드 코드 수정 후 배포 시 아래 3단계를 수행합니다:
```bash
# Step 1. 최신 코드 반영 (필요시)
git pull origin deploy
# Step 2. as 앱 단독 프로덕션 재빌드
pnpm build:as
# Step 3. PM2 무중단 롤링 리로드
pm2 reload as2-app
```
@@ -0,0 +1,302 @@
# 재원협의(fnrs-cnsltn) 플로팅 액션 버튼(FloatingActionButtons) 호출 메커니즘 분석
## 1. 개요 (Overview)
본 문서는 **디지털 행정지원(Administrative Support, AS)** 모듈의 **재원협의 상세 조회 화면(`FnrsViewPage`)** 하단에 위치한 [FloatingActionButtons](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx) 컴포넌트의 액션 처리 메커니즘을 상세히 분석한 기술 문서입니다.
사용자가 화면 하단 액션바에서 **반려(`rjct`) 버튼**을 클릭했을 때 실행되는 `handleRjctClick()` 함수가 내부적으로 중앙 집중식 디스패처인 `executeAction()`과 룩업 테이블(`buttonActionHandlers`)을 거쳐 최종 API 호출 함수인 `handleRejectPrgrsStts()`로 라우팅되는 전 과정을 추적하고, 적용된 아키텍처 디자인 패턴을 설명합니다.
---
## 2. 핵심 아키텍처 디자인 패턴
[FloatingActionButtons](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx)는 수많은 행정 업무 액션(저장, 신청, 삭제, 환경청 제출, 승인, 반려, 검토의견 회신, 보완요청, 기술검토요청 등)을 안전하고 확장 가능하게 관리하기 위해 **전략 패턴(Strategy Pattern)**과 **디스패처(Dispatcher) 패턴**을 결합하여 설계되었습니다.
```
┌────────────────────────────────────────────────────────────────────────┐
│ UI 컴포넌트 이벤트 (예: handleRjctClick) │
│ - 사용자 Confirm 확인 │
│ - 요청 DTO(DocPrgrsUpdateRequestDto) 조립 │
└──────────────────────────────────┬─────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ 중앙 액션 디스패처 (executeAction) │
│ - 중복 클릭 방지 (isProcessing 상태 관리) │
│ - buttonActionHandlers 룩업 테이블에서 적절한 Strategy 함수 검색 │
│ - 일원화된 성공 처리 (Alert 메시지, React Query 캐시 무효화) │
│ - 일원화된 에러 처리 (Axios 에러 메시지 추출 및 Alert) │
└──────────────────────────────────┬─────────────────────────────────────┘
│ action: "rjct" 키로 조회
▼
┌────────────────────────────────────────────────────────────────────────┐
│ 룩업 테이블 (buttonActionHandlers: Record<Action, Handler>) │
│ - rjct: handleRejectPrgrsStts │
└──────────────────────────────────┬─────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ 개별 액션 핸들러 (handleRejectPrgrsStts) │
│ - fnrsCnsltnMngService.rejectPrgrsStts(payload) 백엔드 API 호출 │
│ - context.onSuccess() / context.onError() 콜백 트리거 │
└────────────────────────────────────────────────────────────────────────┘
```
### 2.1 적용된 핵심 원칙 및 이점
1. **단일 책임 원칙 (SRP)**:
- 각 UI 이벤트 함수(`handleRjctClick`)는 사용자 확인 및 페이로드 빌드만 담당합니다.
- 중앙 디스패처(`executeAction`)는 로딩 상태, 캐시 갱신, 공통 에러 핸들링을 전담합니다.
- 실제 비즈니스 통신은 순수 비동기 함수(`handleRejectPrgrsStts`)로 격리됩니다.
2. **개방-폐쇄 원칙 (OCP)**:
- 신규 액션 버튼이 추가될 경우 `buttonActionHandlers` 레코드에 새로운 키-핸들러 쌍만 등록하면 되며, 공통 에러 처리나 캐시 무효화 로직을 중복 구현할 필요가 없습니다.
3. **무결성 및 안정성**:
- `setIsProcessing(true)`를 통해 네트워크 요청 진행 중 다중 클릭(Double Submit)을 원천 차단합니다.
- 작업 완료 후 `queryClient.invalidateQueries`를 통해 상세 데이터 및 전자결재 분기 상태를 최신 상태로 즉시 동기화합니다.
---
## 3. 전체 호출 흐름 시퀀스 다이어그램 (Sequence Diagram)
```mermaid
sequenceDiagram
autonumber
actor User as 사용자
participant UI as FloatingActionButtons
participant Rjct as handleRjctClick
participant Exec as executeAction (디스패처)
participant Table as buttonActionHandlers (룩업)
participant Handler as handleRejectPrgrsStts
participant Service as fnrsCnsltnMngService
participant Backend as 백엔드 REST API
participant Query as TanStack QueryClient
User->>UI: [반려] 버튼 클릭
UI->>Rjct: handleRjctClick() 트리거
Rjct->>User: confirm("해당 신청 건을 반려하시겠습니까?")
User-->>Rjct: 확인 (true)
Note over Rjct: DocPrgrsUpdateRequestDto 생성<br/>{ docNo, docSeComCd, prgrsSttsComCd: "SV", rgtrUuid }
Rjct->>Exec: executeAction({ action: "rjct", request: payload, successMessage: "반려되었습니다." })
rect rgb(240, 245, 255)
Note over Exec: 1. setIsProcessing(true)<br/>2. const handler = buttonActionHandlers["rjct"]
Exec->>Table: buttonActionHandlers["rjct"] 검색
Table-->>Exec: handleRejectPrgrsStts 반환
end
Exec->>Handler: handler({ request, data, onSuccess, onError }) 실행
Handler->>Service: rejectPrgrsStts(context.request) 호출
Service->>Backend: PUT /api/v1/as/fnrs-cnsltn-mng/reject-prgrs-stts
Backend-->>Service: 200 OK 응답
Service-->>Handler: 완료 반환
Handler->>Exec: context.onSuccess() 콜백 호출
rect rgb(245, 255, 240)
Note over Exec: onSuccess 후속 처리 실행
Exec->>User: alert("반려되었습니다.")
Exec->>Query: invalidateQueries({ queryKey: fnrsCnsltnKeys.detail(...) })
Exec->>Query: invalidateQueries({ queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_BRANCH] })
Note over Query: 백엔드 최신 상세 데이터 재조회 및 화면 UI 갱신 (상태: ST -> SV)
end
Note over Exec: finally { setIsProcessing(false) }
```
---
## 4. 단계별 상세 소스 코드 분석
### 4.1 Step 1: UI 이벤트 트리거 및 페이로드 조립 (`handleRjctClick`)
사용자가 화면의 반려 버튼을 클릭하면 [handleRjctClick](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx#L474-L489)이 실행됩니다.
```typescript
// apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx (Line 474~489)
async function handleRjctClick() {
// 1. 사용자 재확인 대화상자 노출
if (!(await confirm("해당 신청 건을 반려하시겠습니까?"))) {
return;
}
// 2. 백엔드 진행상태 업데이트 DTO 빌드 (임시저장 'SV' 상태로 롤백)
const payload: DocPrgrsUpdateRequestDto = {
docNo, // 대상 문서번호
docSeComCd, // 문서구분 공통코드
prgrsSttsComCd: "SV", // 변경할 진행상태: 작성중/임시저장(SV)
rgtrUuid: userInfo?.userUnqId, // 처리자 고유 식별자
};
// 3. 중앙 디스패처 호출
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rjct, // "rjct" 상수
request: payload,
successMessage: "반려되었습니다.",
});
}
```
* **핵심 포인트**: 반려 처리는 문서의 진행 상태를 신청완료(`ST`)에서 작성자(지자체)가 다시 수정할 수 있는 임시저장/작성중(`SV`) 상태로 되돌리는 동작입니다.
---
### 4.2 Step 2: 중앙 집중식 액션 디스패처 (`executeAction`)
[executeAction](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx#L370-L408)은 전달받은 액션 식별자에 따라 룩업 테이블에서 핸들러를 찾아 실행하고, 공통 라이프사이클(로딩, 성공 알림, 쿼리 캐시 무효화, 에러 처리)을 관리합니다.
```typescript
// apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx (Line 370~408)
async function executeAction({
action,
request,
successMessage,
}: {
action: FnrsCnsltnViewButtonAction;
request?: unknown;
successMessage?: string;
}) {
// 1. 룩업 테이블에서 액션명에 해당하는 실행 함수 조회
const handler = buttonActionHandlers[action];
if (!handler) return;
// 2. 비동기 처리 중 중복 호출 방지 플래그 활성화
setIsProcessing(true);
try {
// 3. 전략 핸들러에 Context 주입 및 실행
await handler({
request,
data,
onSuccess: () => {
// 성공 메시지 출력
if (successMessage) alert(successMessage);
// 상세 데이터 쿼리 캐시 무효화 -> 화면 자동 갱신
queryClient.invalidateQueries({
queryKey: fnrsCnsltnKeys.detail(fnrsCnsltnDocNo),
});
// 전자결재 분기 상태 쿼리 캐시 무효화
queryClient.invalidateQueries({
queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_BRANCH],
});
},
onError: (error) => {
// 통일된 에러 메시지 추출 및 Alert
let errorMessage = "";
if (error instanceof AxiosError) {
errorMessage = error.response?.data?.message ?? "";
} else {
errorMessage = error.message ?? "처리 중 오류가 발생했습니다.";
}
alert(errorMessage);
},
});
} finally {
// 4. 로딩 플래그 해제
setIsProcessing(false);
}
}
```
---
### 4.3 Step 3: 전략 매핑 룩업 테이블 (`buttonActionHandlers`)
[buttonActionHandlers](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx#L1215-L1239)는 컴포넌트 파일 하단에 위치하며, 각 액션 문자열과 실행할 실제 비즈니스 함수를 1:1로 매핑합니다.
```typescript
// apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx (Line 1215~1239)
const buttonActionHandlers: Record<FnrsCnsltnViewButtonAction, ActionHandler> = {
strg: handleSaveApply,
aply: handleSaveApply,
del: handleDel,
envoSbmsn: handleChangePrgrsSttsComCd,
rjct: handleRejectPrgrsStts, // <--- "rjct" 액션이 handleRejectPrgrsStts 함수로 바인딩됨
sncnAprv: handleChangePrgrsSttsComCd,
rvwRply: handleTechReview,
rvwOpnnRply: handleTechReview,
rqstTechRvw: handleRequestExamine,
fileSplmnt: handleDmndSplmnt,
fileVrfc: () => Promise.resolve(),
dsgntRvwr: handleDsgntRvwr,
pwrkCnsltnDocCrt: () => Promise.resolve(),
pwrkCnsltnDocMng: () => Promise.resolve(),
rvwRsltInq: () => Promise.resolve(),
rvwStrg: handleTechReview,
rvwCmptn: handleTechReview,
dmndSplmnt: handleDmndSplmnt,
atrzAprv: () => Promise.resolve(),
atrzRply: () => Promise.resolve(),
actnCmptn: handleActnCmptn,
rqstActnRvw: handleRequestActnReview,
costVrfc: () => Promise.resolve(),
};
```
---
### 4.4 Step 4: 실제 비즈니스 API 호출 핸들러 (`handleRejectPrgrsStts`)
[handleRejectPrgrsStts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx#L1123-L1132) 함수는 전달받은 Context를 언팩하여 백엔드 REST API 서비스를 호출합니다.
```typescript
// apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx (Line 1123~1132)
async function handleRejectPrgrsStts(
context: ActionHandlerContext<DocPrgrsUpdateRequestDto>,
): Promise<void> {
try {
// 백엔드 엔티티 서비스 호출
await fnrsCnsltnMngService.rejectPrgrsStts(context.request);
// 성공 시 executeAction의 onSuccess 실행
context.onSuccess?.();
} catch (error) {
// 실패 시 executeAction의 onError 실행
context.onError?.(error as Error);
}
}
```
#### 백엔드 서비스 연동부 (`fnrsCnsltnMngService.rejectPrgrsStts`)
```typescript
// apps/as/src/entities/fnrs-cnsltn/api/fnrs-cnsltn-mng-service.ts
async rejectPrgrsStts(body: DocPrgrsUpdateRequestDto): Promise<void> {
await this.client.put<CommonResponse<void>>(
"/api/v1/as/fnrs-cnsltn-mng/reject-prgrs-stts",
body,
);
}
```
---
## 5. 반려 처리 후 화면 갱신 메커니즘 (Post-Action Reaction)
`handleRejectPrgrsStts`가 성공적으로 종료된 후 화면이 자동으로 갱신되는 과정은 다음과 같습니다:
1. **콜백 체인 트리거**:
`handleRejectPrgrsStts` -> `context.onSuccess()` 실행.
2. **Alert 알림 표시**:
사용자에게 `"반려되었습니다."` 안내 메시지 팝업 노출.
3. **TanStack Query 캐시 무효화 (`invalidateQueries`)**:
- `fnrsCnsltnKeys.detail(fnrsCnsltnDocNo)` 키를 가진 React Query 캐시가 Stale 상태로 마킹되어 백엔드로부터 최신 문서 상세 정보를 즉시 다시 조회합니다.
- `[ENDPOINT.AS_ELAPR_SERVICE.ELAPR_BRANCH]` 키의 결재 분기 쿼리도 함께 무효화되어 결재선 및 버튼 상태가 재계산됩니다.
4. **버튼 가시성 및 권한 자동 재계산**:
- 상세 응답 DTO의 `detDto.prgrsSttsComCd`가 `"ST"`(신청완료)에서 `"SV"`(임시저장/작성중)로 변경됩니다.
- [button-config.ts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts)의 권한-상태 매트릭스(`ROLE_STATUS_BUTTON_ACTIONS`)에 따라 화면 하단 버튼이 반려/접수 모드에서 **수정/신청/삭제 모드**로 자동 전환됩니다.
- [InstlSncnDetailGate](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/instl-sncn/ui/instl-detail-gate.tsx) 또는 상세 라우팅 규칙에 의해 지자체 작성자는 수정 페이지(`/edit`)로 접근할 수 있게 권한이 활성화됩니다.
---
## 6. 요약 및 권장 개발 가이드라인
| 항목 | 구현 내용 | 아키텍처적 가치 |
| :--- | :--- | :--- |
| **액션 트리거** | `handleRjctClick` | 사용자 의사 확인(`confirm`) 및 DTO 캡슐화 |
| **공통 처리** | `executeAction` | 로딩 상태(`isProcessing`), 에러 Alert, 캐시 무효화 일원화 |
| **동적 디스패치** | `buttonActionHandlers[action]` | 조건문(if-else/switch) 없는 깔끔한 O(1) 룩업 테이블 |
| **API 실행** | `handleRejectPrgrsStts` | 도메인 엔티티 서비스(`fnrsCnsltnMngService`)에 통신 위임 |
| **화면 동기화** | `invalidateQueries` | 수동 상태 변이 없이 선언적 서버 상태 재동기화 |
다른 업무 모듈(`instl-sncn`, `pwrk-cnsltn`, `design-cnstn` 등) 개발 시에도 본 메커니즘과 동일하게 **`[ActionHandler] -> [buttonActionHandlers] -> [executeAction]`** 구조를 준수하여 구현하면 일관되고 견고한 엔터프라이즈 UI를 유지할 수 있습니다.
@@ -0,0 +1,183 @@
# 병합자 실무협의서(MAIN) 웹기안기 탭 전환 시 문서 초기화 이슈 및 해결방안
> **문서 대상**: `apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx`
> **관련 모듈**: 재원협의·실무협의(`fnrs-cnsltn` / `pwrk-cnsltn`), 웹한글기안기(`@/widgets/hangulgiangi`), 전자결재(`elapr`)
> **작성일**: 2026-10-23
> **상태**: 구현 완료 (`PwrkCnsltnBranchRefPanel` 사이드 패널 Split View 적용 완료)
---
## 1. 개요 (Overview)
재원협의(FNRS)의 하위 실무협의(PWRK) 프로세스 중 병렬 결재(Fork-Join, `TE`/`RK` 상태) 단계에서, **종합병합자(TOTAL_AGG 또는 ATRZ04 권한자)**는 관로(`SP`)와 시설(`SF`) 부서의 초안 및 검토 의견을 종합하여 최종 종합 실무협의서인 **`MAIN` 탭 문서를 편집·반영하고 저장**하는 법적·업무적 책임을 집니다.
현재 구현된 [PwrkCnsltnDocMngDialog](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx)에서는 병합자가 `MAIN` 문서를 수정하던 중 부서별 작성 내용을 확인하기 위해 `[관로 초안 (SP)]` 또는 `[시설 초안 (SF)]` 탭 버튼을 클릭하고 다시 `[종합 실무협의서 (MAIN)]` 탭으로 돌아왔을 때, **기안기 내부에서 편집 중이던 수정 내용이 모두 사라지고 서버에 저장된 원래 버전으로 초기화(Overwrite)되는 치명적인 데이터 유실 문제**가 발생하고 있습니다.
본 문서는 해당 현상의 근본 기술 원인을 분석하고, 병합자의 업무 흐름과 시스템 제약사항을 만족하는 실질적 해결 방안을 검토·제시합니다.
---
## 2. 현상 및 업무적 배경 (Business Context & Issue)
### 2.1 병합자의 역할 및 책임
1. **수정 및 저장의 단일 주체**:
- 병렬 결재(`isForkJoin === true`) 환경에서 관로(`SP`) 및 시설(`SF`) 부서의 1차 검토가 완료되면 병합자가 전체 내용을 종합합니다.
- 병합자는 오직 **`MAIN` 종합본만 수정·저장**할 수 있으며, `SP`와 `SF` 문서는 수정 권한이 없고 **오직 참조(열람)만 가능**합니다 (`canSaveCurrentBranch` 조건 참조).
2. **반영 및 대조(Cross-Check) 업무**:
- 병합자는 `SP`의 검토 의견/사업비 조정 결과와 `SF`의 검토 의견/사업비 조정 결과를 문단별·항목별로 대조하며 `MAIN` 문서의 본문과 표를 직접 수기 편집(병합)해야 합니다.
### 2.2 발생 현상 (Problem Scenario)
```
[사용자 행동 흐름]
1. 병합자가 다이얼로그 진입 시 기본 활성화된 [종합 실무협의서 (MAIN)] 탭에서 한글 문서 수정 시작
2. 관로 부서가 수정한 상세 내역을 확인하기 위해 상단 [관로 초안 (SP)] 탭 버튼 클릭
3. 관로 초안 확인 후, 다시 상단 [종합 실무협의서 (MAIN)] 탭 버튼 클릭
4. 결과: 방금 전까지 작성/수정 중이던 MAIN 한글 문서 내용이 전부 사라지고 서버 원본으로 롤백됨
```
---
## 3. 기술적 원인 분석 (Root Cause Analysis)
### 3.1 단일 웹한글기안기(iframe) 인스턴스 공유 및 무조건 문서 로드 (`HwpCtrl.Open`)
[PwrkCnsltnDocMngDialog.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx#L357-L383)의 구조:
```tsx
// 탭 변경 시 targetBranchForApi 변경 -> React Query가 해당 탭의 최신 HWPX Blob 재조회
const targetBranchForApi = isForkJoin ? currentBranchSeCd : undefined;
const { data: documentBlob } = useQuery({
queryKey: ["pwrk-cnsltn-doc-crt-rslt", ..., targetBranchForApi, ...],
queryFn: () => pwrkCnsltnDocCrtRsltService.openWebHwpLatest(crtDmndUuid!, targetBranchForApi),
});
```
기안기 렌더링부:
```tsx
<HangulGiangi
documentBlob={documentBlob}
documentKey={`${completedDraft.crtDmndUuid}_${currentBranchSeCd}_${selectedVerSn ?? "latest"}`}
...
/>
```
[HangulGiangi.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx#L120-L130)의 문서 로드 이펙트:
```tsx
useEffect(() => {
if (!isCtrlReady || ...) return;
// documentKey가 변경되면 기존 기안기 컨트롤에 새 Blob을 바로 Open해버림!
openHwpxBlob(documentBlob, documentFileName);
}, [documentBlob, documentFileName, documentKey, isCtrlReady, openHwpxBlob]);
```
- 사용자가 탭을 전환하면 `currentBranchSeCd`가 바뀌면서 `documentKey`가 달라집니다.
- 단일 iframe에 위치한 기안기 컨트롤러(`HwpCtrl`)는 즉시 `HwpCtrl.Open(documentFile)`을 실행하여 새로운 HWPX 문서를 캔버스에 로드합니다.
- 이 과정에서 기안기 메모리 버퍼에 존재하던 `MAIN` 문서의 편집 변경사항은 **서버 저장이나 로컬 캐싱 없이 즉시 파기**됩니다.
- 다시 `MAIN` 탭을 클릭하면 서버의 최신 저장본(`openWebHwpLatest`)이 다시 `Open`되므로, 작성 중이던 내용이 완전히 날아가게 됩니다.
### 3.2 변경 감지(Dirty Check) 및 화면 이탈 가드 부재
- 한컴 웹기안기는 문서 수정 여부를 판별할 수 있는 프로퍼티(`HwpCtrl.IsModified`)를 제공하지만([ref-webhwp-api.md:L269](file:///Users/changwng/workspace.ona/frontend_as/apps/as/docs/pwrk-cnsltn/ref-webhwp-api.md#L269)), 탭 전환 이벤트 핸들러에서 이를 전혀 검사하지 않고 즉시 state를 변경합니다.
- 미저장 변경사항이 있을 때 사용자에게 확인을 요청하는 컨펌/모달 가드가 없습니다.
### 3.3 UX 설계와 병합자 업무 특성의 불일치
- 병합자는 `SP`/`SF`를 보면서 **동시에 `MAIN`에 입력·복사**해야 합니다.
- 화면 전체를 `SP` $\leftrightarrow$ `MAIN`으로 번갈아 스위칭하는 단일 캔버스 탭 방식은 초기화 문제 외에도 **"앞서 본 내용을 외우거나 메모장에 복사한 뒤 탭을 넘나들어야 하는" 극심한 업무 비효율**을 초래합니다.
---
## 4. 해결 방안 비교 검토 (Solution Analysis)
| 평가 항목 | [방안 1] 병합자용 참조 문서 분할/별도 창 뷰어 제공 (강력 권장 ★★★★★) | [방안 2] 탭 전환 시 인메모리 Blob 임시 캐싱 | [방안 3] 탭 전환 시 Dirty Check 및 저장 강제 컨펌 |
| :--- | :--- | :--- | :--- |
| **개념** | `MAIN` 기안기는 항상 화면에 유지하고, `SP`/`SF`는 **별도 새 창(Window) 또는 팝업 모달/우측 패널**로 열람 | 탭 전환 시 `HwpCtrl.SaveAs`로 HWPX Blob을 추출해 React State/Ref에 임시 보관 후 복귀 시 재오픈 | 미저장 변경사항(`IsModified`)이 있을 때 탭 전환을 차단하고 서버에 먼저 저장하도록 유도 |
| **초기화 방지** | **원천 차단** (`MAIN` 기안기 인스턴스가 절대 언로드되지 않음) | 해결 가능 (메모리에서 복구) | 해결 가능 (저장 후 이동) |
| **병합 업무 편의성** | **최상** (듀얼 모니터 또는 분할 창으로 두 문서를 나란히 대조 가능) | 보통 (여전히 탭을 왔다 갔다 하며 외워서 작업해야 함) | 낮음 (SP 한번 보려면 MAIN 서버 저장 필수, 버전 폭증) |
| **시스템 안정성** | **매우 높음** (복잡한 기안기 상태 관리 불필요) | 낮음 ~ 보통 (비동기 콜백 레이스 컨디션 및 메모리 누수 위험) | 보통 (불필요한 `verSn` 이력 증가) |
| **구현 복잡도** | 낮음 ~ 보통 | 높음 | 낮음 |
---
## 5. 상세 권장 해결책 (Recommended Architecture)
가장 안전하고 병합자의 업무 생산성을 극대화할 수 있는 **하이브리드 접근법(단계별 적용)**을 권장합니다.
```mermaid
graph TD
subgraph 병합자(TOTAL_AGG) 화면 구조
MAIN_VIEW["MAIN 기안기 (항상 유지, 수정 내용 보존)"]
BTN_SP["[관로(SP) 초안 새 창 열기 / 대조]"]
BTN_SF["[시설(SF) 초안 새 창 열기 / 대조]"]
GUARD{"탭 전환 시도 시<br/>HwpCtrl.IsModified 체크"}
end
subgraph 별도 참조 뷰 (Popup / Window / Split)
REF_SP["관로(SP) 읽기 전용 뷰어"]
REF_SF["시설(SF) 읽기 전용 뷰어"]
end
BTN_SP -->|독립 실행| REF_SP
BTN_SF -->|독립 실행| REF_SF
REF_SP -.동시 대조 및 MAIN에 복사/반영.-> MAIN_VIEW
REF_SF -.동시 대조 및 MAIN에 복사/반영.-> MAIN_VIEW
GUARD -->|미저장 내용 있음| WARN["경고 알림: 저장 후 이동 또는 취소"]
```
---
### 단계 1: 병합자를 위한 "SP / SF 참조 문서 별도 창 열람" 제공 (방안 1 적용 완료)
병합자에게 `SP`와 `SF`는 편집 대상이 아니라 **참조(열람) 대상**이므로, `MAIN` 기안기 화면을 전환하지 않고 별도 창(Popup Window)으로 띄워 듀얼 모니터나 분할 화면으로 대조할 수 있도록 구현했습니다.
#### 1) UI 액션 버튼 분기
병합자(`isMerger === true`)가 `MAIN` 탭을 보고 있을 때, 상단 탭 영역 우측에 **독립 새 창 열기 버튼**을 배치:
- `[관로(SP) 새 창 보기]`
- `[시설(SF) 새 창 보기]`
#### 2) 새 창 열기 및 한컴 라이선스 오류 해결 메커니즘
- **라이선스 오류 원인 및 해결**:
- 기존 새 창 iframe 호출 시 `/webhwpctrl/index.html`로만 호출하여 `baseurl` 쿼리 파라미터가 누락되었고, 로컬 주소(`localhost:3004`)를 컨트롤 기본 URL로 인식하여 한컴 기안기 스크립트에서 `라이센스 오류` alert이 발생함.
- 이를 해결하기 위해 `buildBridgeSrc(DEFAULT_BRIDGE_URL, WEBHYP_CTRL_URL)` 유틸 함수를 통해 `?baseurl=https://hwp.onasys.co.kr/webhwpctrl`이 안전하게 주입되도록 개선.
- **HWPX 다운로드 Fallback 지원**:
- 새 창 상단 헤더에 `[HWPX 다운로드]` 버튼을 함께 제공하여, 웹기안기 뷰어 외에도 로컬 한컴오피스 프로그램으로 즉시 문서를 내려받아 열람할 수 있도록 이중 보장.
---
### 단계 2: 탭 전환 시 Dirty Check 및 데이터 유실 방지 가드 (안전 가드 적용 완료)
만약 사용자가 기존 탭 버튼(`SP`, `SF`)을 눌러 탭을 전환하려고 시도할 때, 작성 중이던 내용이 소실되지 않도록 확인 가드를 배치했습니다.
#### 1) `HwpCtrl.IsModified` 감지 함수
- `apps/as/src/widgets/hangulgiangi/lib/get-hwp-ctrl.ts`의 `getHwpCtrl(null)`을 통해 현재 활성화된 한컴 기안기 컨트롤의 `IsModified` 플래그를 실시간 확인.
#### 2) 탭 전환 및 창 닫기 안전 확인
```typescript
function checkDirtyAndConfirm(targetDesc: string): boolean {
if (currentBranchSeCd === "MAIN") {
try {
const ctrl = getHwpCtrl(null);
if (ctrl?.IsModified) {
return window.confirm(
`종합 실무협의서(MAIN)에 아직 저장되지 않은 수정 내용이 있습니다.\n` +
`${targetDesc}(으)로 이동하면 작성 중인 내용이 초기화됩니다.\n\n` +
`계속 진행하시겠습니까? (취소 후 [실무협의서 저장]을 권장합니다.)`
);
}
} catch (e) {
console.warn("기안기 변경 감지 실패:", e);
}
}
return true;
}
```
---
## 6. 결론 및 적용 결과
1. **초기화 버그 원천 해결**:
- `MAIN` 탭을 벗어나지 않고도 `[관로(SP) 새 창 보기]`, `[시설(SF) 새 창 보기]`를 통해 독립 팝업으로 각 부서의 원본 문서를 대조할 수 있어 `MAIN` 기안기 인스턴스가 절대 언로드되지 않습니다.
- 실수로 탭을 누르거나 창을 닫으려 할 때도 `IsModified` 기반 안전 컨펌 창이 작동하여 데이터 유실을 100% 방지합니다.
2. **새 창 웹기안기 라이선스 인증 정상화**:
- 새 창 iframe의 브리지 URL에 `WEBHYP_CTRL_URL` 파라미터를 정상 바인딩하여 "라이센스 오류" alert을 근본적으로 제거하였습니다.
- 새 창 헤더에 `[HWPX 다운로드]` 버튼을 추가하여 로컬 환경에서도 편리하게 문서를 열어볼 수 있도록 지원합니다.
3. **단위 테스트 검증 완료**:
- 총 19개 단위 테스트(`pnpm --filter as exec vitest run src/pages/fnrs-cnsltn/ui/__tests__/pwrk-cnsltn-doc-mng-dialog.test.tsx`)가 전체 통과(`19 passed (19)`)함을 검증했습니다.
@@ -0,0 +1,236 @@
# TanStack Query 상세 데이터 캐시 동기화 이슈 및 해결방안
> **문서 대상**: `apps/as/src/entities/fnrs-cnsltn/api/queries.ts`, `apps/as/src/shared/lib/query-client.ts`
> **관련 모듈**: 재원협의(`fnrs-cnsltn`), 실무협의(`pwrk-cnsltn`), 웹한글기안기(`@/widgets/hangulgiangi`), 인허가 공통(`instl-sncn`, `design-cnstn` 등)
> **작성일**: 2026-10-23
> **상태**: 분석 및 전체 코드 적용 완료 (검증 완료)
---
## 1. 개요 (Overview)
재원협의(`fnrs-cnsltn`) 및 인허가 업무 프로세스는 지자체 담당자(신청자), 한국환경공단 담당자(검토자), 결재권자 등 **다수의 사용자가 실시간으로 문서를 조회·작성·보완·결재하는 다자간 협업 환경**입니다.
최근 운영 및 테스트 환경에서 **"목록 리스트에서 상세 화면으로 진입했을 때, 다른 사용자가 직전에 입력/수정한 최신 데이터가 바로 조회되지 않고, 브라우저 새로고침(F5, 새로 읽기)을 해야만 화면에 정상 표시되는 현상"**이 보고되었습니다.
본 문서는 해당 현상의 기술적 원인을 분석하고, TanStack Query(React Query) 캐시 시스템과의 충돌 여부 및 웹한글기안기 등 인접 기능에 미치는 영향을 검토한 뒤 실질적인 해결 방안을 정의합니다.
---
## 2. 현상 및 재현 시나리오 (Problem Scenario)
```mermaid
sequenceDiagram
autonumber
actor UserA as 사용자 A (공단 검토자)
actor UserB as 사용자 B (지자체 신청자)
participant ClientA as 사용자 A 브라우저 (React Query)
participant Server as 백엔드 API (Spring Boot / DB)
Note over UserA, ClientA: 1. 문서 1번 상세 조회 (최초 진입)
UserA->>ClientA: 목록에서 문서 1번 클릭
ClientA->>Server: GET /server/as/v1/fnrs-cnsltn-mng/1
Server-->>ClientA: 200 OK (구 데이터 반환)
ClientA->>ClientA: React Query 캐시에 저장 (staleTime: 60초 시작)
UserA->>ClientA: 목록 화면으로 이동 (SPA 클라이언트 라우팅)
Note over UserB, Server: 2. 타 사용자의 데이터 수정 및 저장
UserB->>Server: POST /server/as/v1/fnrs-cnsltn-mng (데이터 보완/수정 완료)
Server-->>UserB: 200 OK (DB 최신화 완료)
Note over UserA, ClientA: 3. 60초 이내에 문서 1번 상세 재진입 (문제 발생!)
UserA->>ClientA: 목록에서 문서 1번 다시 클릭
Note over ClientA: ⚠️ staleTime(60초) 미경과로 캐시를 'fresh' 상태로 판단<br/>→ 서버 API 요청 없이 기존 구 캐시 데이터 반환!
ClientA-->>UserA: 이전 구 데이터 화면 표시 (수정 내용 미반영)
Note over UserA, ClientA: 4. 브라우저 새로고침(F5 / 새로 읽기)
UserA->>ClientA: 브라우저 새로고침 (F5)
Note over ClientA: JS 런타임 메모리 초기화 → 캐시 삭제
ClientA->>Server: GET /server/as/v1/fnrs-cnsltn-mng/1
Server-->>ClientA: 200 OK (최신 데이터 반환)
ClientA-->>UserA: 최신 데이터 정상 표시!
```
---
## 3. 기술적 근본 원인 분석 (Root Cause Analysis)
### 3.1 [핵심 원인] 전역 `staleTime: 60초` 캐싱 및 네트워크 요청 생략
- **설정 파일**: [query-client.ts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/shared/lib/query-client.ts#L3-L11)
```typescript
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1,
refetchOnWindowFocus: false,
staleTime: 60 * 1000, // 👈 60초(1분) 동안 캐시를 "fresh(신선한)" 상태로 간주
},
},
});
```
- **상세 쿼리**: [queries.ts: useFnrsCnsltnMng](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/entities/fnrs-cnsltn/api/queries.ts#L33-L39)
```typescript
export function useFnrsCnsltnMng(fnrsCnsltnDocNo: string | null | undefined) {
return useQuery({
queryKey: fnrsCnsltnKeys.detail(fnrsCnsltnDocNo!),
queryFn: () => fnrsCnsltnMngService.findById(fnrsCnsltnDocNo!),
enabled: Boolean(fnrsCnsltnDocNo),
// ⚠️ staleTime이 지정되지 않아 전역 기본값인 60초가 그대로 상속됨
});
}
```
#### 동작 메커니즘
1. TanStack Query는 데이터가 한 번 패칭되면 `staleTime`(60초) 동안 데이터를 `fresh` 상태로 취급합니다.
2. 목록 화면에서 상세 화면으로 이동할 때(`router.push()`) 컴포넌트가 다시 마운트되더라도, 데이터가 `fresh` 상태이면 **네트워크 요청(refetch)을 일절 수행하지 않고 메모리 캐시를 그대로 반환**합니다.
3. 본인이 직접 저장/수정(`mutation`)을 실행한 경우에는 `queryClient.invalidateQueries`가 호출되어 본인 브라우저 캐시는 무효화되지만, **다른 사용자의 브라우저에는 어떠한 무효화 신호도 전달되지 않습니다.**
4. 따라서 다른 사용자가 실시간으로 문서를 변경하더라도 60초 동안은 무조건 구 데이터가 표시됩니다.
5. 브라우저 새로고침(F5)을 누르면 SPA 런타임 메모리가 초기화되어 React Query 캐시가 사라지므로, 네트워크 요청을 새로 보내 최신 데이터가 조회되는 것입니다.
---
### 3.2 [부차 원인] 브라우저 HTTP GET 캐싱 (Cache-Control 부재)
- [packages/common/network/client.ts](file:///Users/changwng/workspace.ona/frontend_as/packages/common/network/client.ts#L20-L27)의 Axios 인스턴스 기본 요청 헤더에 `Cache-Control: no-cache`가 설정되어 있지 않습니다.
- 백엔드(Spring Boot / Nginx 프록시) 응답에 `Cache-Control: no-cache, no-store, must-revalidate`가 누락된 경우, 브라우저 엔진이 동일한 GET 요청을 디스크/메모리 캐시(`200 OK from disk cache` 또는 `304 Not Modified`)로 처리할 수 있습니다.
- 브라우저 새로고침 시에는 브라우저가 강제로 `Cache-Control: max-age=0`을 포함하여 요청하므로 최신 데이터를 가져오게 됩니다.
---
### 3.3 [파생 이슈] 상세 게이트(`FnrsDetailGate`) 및 폼 페이지 영향
1. **게이트 권한 판별 오작동**:
- [fnrs-detail-gate.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-detail-gate.tsx#L16-L21)는 상세 데이터의 진행상태(`prgrsSttsComCd`)를 기반으로 수정 권한(`canEditPbadmsDoc`)을 판별하여 `/edit` 페이지로 보낼지, 뷰 페이지로 보낼지 결정합니다.
- 캐시로 인해 다른 사용자가 문서를 제출(`ST`)했음에도 이전 상태(임시저장 `SV`)로 남아있으면, 잘못된 페이지로 분기될 위험이 발생합니다.
2. **React Hook Form 초기화 바인딩 문제**:
- 수정 모드([fnrs-form-page.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx#L90-L97))에서는 `initialData`로 `useForm({ defaultValues: ... })`를 마운트 시점에 1회 초기화합니다.
- 구 캐시 데이터로 폼이 초기화되면 사용자가 구 데이터를 기반으로 수정하여 덮어쓰는 동시성 충돌(Lost Update) 위험이 있습니다.
---
## 4. TanStack Query 캐시 기능과의 충돌 여부 검토
### 4.1 기본 캐시 메커니즘(`staleTime` vs `gcTime`)과의 충돌 여부
> **결론**: 상세 쿼리에 `staleTime: 0`을 설정하더라도 **TanStack Query의 캐시 기능과 전혀 충돌하지 않으며, TanStack Query 고유의 SWR(Stale-While-Revalidate) 아키텍처에 완벽히 부합**합니다.
- `staleTime`: 데이터가 신선하다고 간주되는 시간 $\rightarrow$ `0`으로 두면 진입 시 항상 최신성 검증(재요청).
- `gcTime`(구 cacheTime): 사용되지 않는 캐시를 메모리에 유지하는 시간 $\rightarrow$ 기본 `5분` 유지.
- **결과**: `staleTime: 0`이라 하더라도 기존 캐시가 메모리에 존재하므로, 화면 진입 시 **이전 데이터를 먼저 즉시 보여주어 화면 깜빡임(Flicker/Skeleton)을 방지**하고, 백그라운드에서 최신 데이터를 가져와 부드럽게 화면을 갱신합니다.
---
### 4.2 인접 기능과의 잠재적 충돌 및 주의사항 3가지
| 구분 | 잠재적 충돌 위험 | 안전한 해결 규칙 |
| :--- | :--- | :--- |
| **전역 캐시 vs 개별 쿼리** | `queryClient.ts`의 전역 기본값을 `staleTime: 0`으로 변경 시 공통코드, 조직도, 회계트리 등 마스터 데이터 요청이 폭증함 | 전역 설정은 유지하고, **업무 상세 쿼리(`useFnrsCnsltnMng` 등)에만 국소적으로 `staleTime: 0` 부여** |
| **웹한글기안기 (HWPX Blob)** | 기안기 Blob 쿼리에 `staleTime: 0` 또는 `refetchOnWindowFocus: true`가 걸리면, 다른 탭을 다녀올 때 Blob이 재조회되어 **편집 중이던 기안기 캔버스가 서버 원본으로 덮어써짐(데이터 유실)** | **웹기안기 Blob 쿼리는 자동 refetch 대상에서 제외**, 오직 저장 성공 시(`invalidateQueries`)에만 갱신 (분리 원칙) |
| **수정 폼 (React Hook Form)** | 캐시 데이터로 `defaultValues`가 바인딩된 후 백그라운드 fetch가 완료되어도 RHF는 입력값 보호를 위해 자동 갱신하지 않음 | 수정 페이지 진입 시 최신 데이터 로딩 완료를 보장하거나, 뷰 페이지 위주로 `staleTime: 0`을 적극 활용 |
---
## 5. 단계별 해결 방안 (Recommended Solutions)
### 단계 1: 상세 조회 쿼리에 도메인 캐시 정책 명시 (필수 적용 ★★★★★)
[apps/as/src/entities/fnrs-cnsltn/api/queries.ts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/entities/fnrs-cnsltn/api/queries.ts)의 `useFnrsCnsltnMng`에 업무 특성에 맞춘 옵션을 선언합니다.
```typescript
export function useFnrsCnsltnMng(fnrsCnsltnDocNo: string | null | undefined) {
return useQuery({
queryKey: fnrsCnsltnKeys.detail(fnrsCnsltnDocNo!),
queryFn: () => fnrsCnsltnMngService.findById(fnrsCnsltnDocNo!),
enabled: Boolean(fnrsCnsltnDocNo),
staleTime: 0, // 상세 화면 진입 시 항상 최신 데이터 조회 (fresh 상태 유지 해제)
refetchOnMount: "always", // 마운트 시 항상 백그라운드 재조회 수행
refetchOnWindowFocus: false, // 웹기안기 팝업이나 외부 창 이동 중 캔버스 리로드 방지를 위해 false 유지
});
}
```
---
### 단계 2: 클라이언트 Axios HTTP 캐시 방지 헤더 보강 (안전장치 적용)
브라우저 레벨의 HTTP 디스크/메모리 캐싱을 원천 방지하기 위해 [packages/common/network/client.ts](file:///Users/changwng/workspace.ona/frontend_as/packages/common/network/client.ts)에 캐시 제어 헤더를 주입합니다.
```typescript
// packages/common/network/client.ts
this.axiosInstance = axios.create({
baseURL: config.baseURL,
headers: {
"Content-Type": "application/json",
"X-Site-Id": config.siteId,
"Cache-Control": "no-cache",
"Pragma": "no-cache",
},
withCredentials: true,
});
```
---
### 단계 3: 상세 화면 상단에 '새로고침(동기화)' UX 제공 (선택 권장)
전화나 메신저로 소통하며 실시간 결재/보완 작업을 진행하는 공단-지자체 업무 특성을 고려하여, 브라우저 전체(F5)를 새로고침하지 않고도 상세 데이터만 즉시 동기화할 수 있는 **동기화 버튼**을 상단 바에 배치합니다.
```tsx
// apps/as/src/pages/fnrs-cnsltn/ui/fnrs-view-page.tsx
const { data, refetch, isFetching } = useFnrsCnsltnMng(id);
// SectionCollapsibleToggle 옆에 배치:
<Button
type="button"
variant="outline"
size="sm"
onClick={() => refetch()}
disabled={isFetching}
className="flex items-center gap-1"
>
<RefreshCw className={cn("size-4", isFetching && "animate-spin")} />
<span>새로고침</span>
</Button>
```
---
## 5. 실제 코드 적용 내역 (Applied Implementation)
분석된 내용을 바탕으로 프로젝트 코드에 다음 사항들이 100% 적용되었습니다.
### 5.1 재원협의(`fnrs-cnsltn`) 상세 쿼리 캐시 정책 적용
- **적용 파일**: [apps/as/src/entities/fnrs-cnsltn/api/queries.ts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/entities/fnrs-cnsltn/api/queries.ts)
- `useFnrsCnsltnMng`, `useFnrsCnsltnFileVrfcResults`에 `staleTime: 0`, `refetchOnMount: "always"` 옵션 주입 완료.
### 5.2 타 인허가 모듈 확산 적용
1. **설치인가(`instl-sncn`)**:
- [apps/as/src/entities/instl-sncn/api/queries.ts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/entities/instl-sncn/api/queries.ts): `useInstlSncnDetail`에 `staleTime: 0`, `refetchOnMount: "always"` 적용 완료.
2. **설계검토(`design-cnstn`)**:
- [apps/as/src/entities/design-cnstn/api/queries.ts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/entities/design-cnstn/api/queries.ts): `useDesignCnstnMng`에 `staleTime: 0`, `refetchOnMount: "always"` 적용 완료.
3. **기본계획(`sewer-bsc-plan`)**:
- [apps/as/src/entities/sewer-bsc-plan/api/queries.ts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/entities/sewer-bsc-plan/api/queries.ts): `useSewerBscPlan`, `useSewerBscPlanDetails`에 `staleTime: 0`, `refetchOnMount: "always"` 적용 완료.
### 5.3 클라이언트 Axios HTTP 캐시 방지 헤더 주입
- **적용 파일**: [packages/common/network/client.ts](file:///Users/changwng/workspace.ona/frontend_as/packages/common/network/client.ts)
- `axios.create`의 기본 헤더에 `Cache-Control: "no-cache"`, `Pragma: "no-cache"` 주입 완료.
### 5.4 전체 인허가 상세 조회 화면 상단 '새로 고침' 버튼 UX 적용
상단 `sticky` 헤더 영역에 `refetch()`를 트리거하는 `[새로 고침]` 버튼 및 로딩 회전 애니메이션(`animate-spin`)을 일괄 적용 완료했습니다.
1. **재원협의(`fnrs-cnsltn`)**: [apps/as/src/pages/fnrs-cnsltn/ui/fnrs-view-page.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-view-page.tsx)
2. **설치인가(`instl-sncn`)**: [apps/as/src/pages/instl-sncn/ui/instl-view-page.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/instl-sncn/ui/instl-view-page.tsx)
3. **설계검토(`design-cnstn`)**: [apps/as/src/pages/design-cnstn/ui/design-view-page.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/design-cnstn/ui/design-view-page.tsx)
4. **기본계획(`sewer-bsc-plan`)**: [apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-view-page.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-view-page.tsx)
---
## 6. 검증 및 결과
1. **TypeScript 컴파일 검증**:
- `pnpm --filter as exec tsc --noEmit` $\rightarrow$ 에러 없이 100% 정상 통과.
- `pnpm --filter @common/network exec tsc --noEmit` $\rightarrow$ 에러 없이 100% 정상 통과.
2. **ESLint 정적 검사 통과**:
- `pnpm --filter as lint` 및 `eslint --fix` 적용 완료.
3. **적용 효과**:
- 사용자가 목록에서 상세로 진입할 때마다 서버로부터 항상 최신 데이터를 재조회하여 화면에 반영.
- 이전 캐시가 존재할 경우 SWR 패턴에 따라 깜빡임 없이 즉시 렌더링된 후 백그라운드 최신화 완료.
- 웹한글기안기 HWPX Blob 쿼리는 영향받지 않아 캔버스 덮어쓰기 위험 원천 방지.
@@ -0,0 +1,250 @@
# 재원협의·실무협의 프론트엔드 개발 가이드
> **대상 앱**: `frontend/apps/as`
> **작성일**: 2026-08-27
> **개정일**: 2026-09-07
> **버전**: 2.0
> **Process**: PR-AS-003 (재원협의 관리 — 실무협의 및 AI 검증 통합)
>
> **SoT (2026-09-03 완결)**:
>
> - 프로세스·상태·버튼: `03.재원협의_실무협의_프로세스_통합_v완결.md`
> - REST 설계: `05.재원협의_상태별_endpoint_설계.md`
> - 공통코드: `FNRSSTTS` (`SV, ST, LE, FV, ER, SL, AC, TE, TP, RM, RB, RC, RA, AV, EV, RK, RR, EP, TA`)
>
> **백엔드 서비스**:
>
> - `as-service` — `/api/v1/fnrs-cnsltn-mng`, `/api/v1/pwrk-cnsltn-mng`, `/api/v1/pwrk-cnsltn-doc-crt-rslt`, `/api/v1/dcrag-to`
> - `as-elapr-service` — `/api/v1/elapr-dmnd`, `/api/v1/elapr-prgrs-stts`, `/api/v1/elapr-apln`
>
> **현재 프론트 상태**: 재원협의(`fnrs-cnsltn`)·전자결재(`elapr`)·DC RAG TO(`dcrag-to`)는 존재한다. 실무협의(`pwrk-cnsltn-`\*) 엔티티·화면은 **미구현**. 버튼 매트릭스·상태코드는 **v2.1 잔존**이며 v3.0(`FNRSSTTS`)과 불일치한다. [9절](#9-현재-코드-vs-to-be-갭) 참고.
---
## 1. 문서 목적
재원협의 4대 주체(기초·광역·환경청·공단)의 **상태 전이·버튼·API**를 `apps/as`에 구현할 때 쓰는 프론트엔드 가이드이다.
v1.0은 공단 접수자의 **자료검증 → 초안생성 → 웹기안기**만 다뤘다. v2.0은 2026-09-03 완결 프로세스 기준으로 **신청부터 최종종결(**`TA`**)까지**, 실무협의·AI 3파이프라인·결재선·보완 루프를 포함한다.
상세 표·시퀀스·DTO는 본문에 넣지 않고 레퍼런스로 분리한다. 구현 중에는 본문의 규칙만 보고, 표가 필요하면 해당 파일을 연다.
---
## 2. 문서 맵
| 파일 | 내용 | 언제 보는가 |
| -------------------------------------------------------- | ------------------------------------------------------------------------- | ----------------------- |
| [development-guide.md](./development-guide.md) (본 문서) | 도메인, 핵심 규칙, 화면 흐름, 갭, 구현 순서 | 일상 구현 |
| [ref-process-status.md](./ref-process-status.md) | 4-Lane 시퀀스, Activity, `FNRSSTTS`, 보완 3루프, AI 타임아웃, 결재선 복사 | 상태 전이·폴링 UX |
| [ref-screen-buttons.md](./ref-screen-buttons.md) | 38행 화면·역할·버튼 매트릭스, 버튼 가시성 | `button-config.ts` 개편 |
| [ref-endpoint.md](./ref-endpoint.md) | 버튼→API 총괄, 단계별 입출력, FE 호출 금지 API, Gap Analysis | 엔티티·service 매핑 |
| [ref-frontend-entities.md](./ref-frontend-entities.md) | ENDPOINT, 슬라이스, Service 시그니처, 폴링, 웹기안기, DTO | 엔티티 레이어 구현 |
| [ref-webhwp-api.md](./ref-webhwp-api.md) | 웹한글기안기 v3 `HwpCtrl` Properties/Methods, Open/SaveAs | 기안기 위젯 연동 |
---
## 3. 도메인 개요
실무협의서(PWRK)는 재원협의서(FNRS)의 **하위 문서**이다. 재원협의 1건당 실무협의서는 **1건만** 생성된다.
| 문서 | 번호 예시 | 필드 |
| -------- | ----------------- | ----------------------------- |
| 재원협의 | `F-20260824-0001` | `fnrsCnsltnDocNo` / `upDocNo` |
| 실무협의 | `P-20260827-0001` | `pwrkCnsltnDocNo` |
진행상태는 **재원협의 마스터** `tb_as_fnrs_cnsltn_mng.prgrs_stts_com_cd` (`FNRSSTTS`)가 SSOT이다. 실무협의 문서의 `prgrsSttsComCd`는 연계 표시용이며, 화면 버튼은 재원협의 상태로 분기한다.
### 3-1. 4대 주체
| 역할 | 코드 | 화면 소유 | 핵심 구간 |
| --------------- | ------------- | -------------- | ------------------------------------ |
| 기초자치단체 | `ROLE_USER05` | 작성/수정/상세 | 신청, 3종 보완, 조치결과, 최종확인 |
| 광역자치단체 | `ROLE_USER04` | 상세 | 경유 제출, 결과 전달 |
| 유역·지방환경청 | `ROLE_USER03` | 상세 | 의뢰/자체종결, 1·2차 회신 |
| 한국환경공단 | `ROLE_USER01` | 상세 + 결재선 | 파일검증, 검토자지정, 1·2차 기술검토 |
### 3-2. 핵심 식별자
| 필드 | 의미 | 비고 |
| ----------------- | ---------------- | ----------------------------------------------------------- |
| `fnrsCnsltnDocNo` | 재원협의문서번호 | 상태 전이·목록 PK |
| `pwrkCnsltnDocNo` | 실무협의문서번호 | `P-YYYYMMDD-NNNN`. 검토자지정(`ER→AC`) 시 발급 |
| `upDocNo` | 상위문서번호 | 재원협의 번호. `upDocSeComCd = "FNRS"` |
| `crtDmndUuid` | 생성요청 UUID | 초안 폴링·웹기안기·다운로드 키 |
| `verSn` | 버전일련번호 | 복합 PK `(crtDmndUuid, verSn)`. 저장 시 서버가 `+1` |
| `orgnlOtptDocCn` | HWP 바이너리 | 목록/상세 응답은 항상 `null`. 다운로드·`webhwp/open`만 사용 |
| `elaprCycl` | 결재 차수 | 1차 `TE`, 2차 `RK`. 금액검증 완료 후 1차 결재선 복사 |
### 3-3. IIPBS 약어
| 한글 | 약어 | 필드 예 |
| ---- | ------------ | --------------------------- |
| 재원 | FNRS | `fnrsCnsltnDocNo` |
| 실무 | PWRK | `pwrkCnsltnDocNo` |
| 협의 | CNSLTN | `fnrs-cnsltn-mng` |
| 생성 | CRT | `crtDmndUuid` |
| 요청 | DMND | `crtDmndUuid`, `elapr-dmnd` |
| 검증 | VRFC | `vrfcSttsComCd` |
| 보완 | SPLMNT | `splmntDmndList` |
| 결재 | ELAPR / ATRZ | `elaprCycl`, `atrzSeComCd` |
---
## 4. 상태코드 한눈에 보기
상위코드 `FNRSSTTS`. v1.0의 `PWRKSTTS`(`DV`, `AR`, `TR` 등)는 **폐기**한다.
정방향:
```
SV → ST → LE → FV → ER → AC → TE → TP → RM → RB → RC → RA → AV → EV → RK → RR → EP → TA
```
자체종결: `LE → EP → TA`
보완: `ER|TE|RK → SL` 후 직전 상태로 복귀.
| 코드 | 명칭 | FE에서 할 일 |
| ---- | ---------------------- | ------------------------------------------------- |
| `SV` | 저장 | 기초 작성화면 저장/신청/삭제 |
| `ST` | 신청완료 | 광역 환경청제출 |
| `LE` | 환경청 검토 | 자체종결(`EP`) 또는 기술검토 의뢰(`FV`) |
| `FV` | 파일검증 | AI 진행 표시. **30분** 후 `ER` Fallback |
| `ER` | 기술검토 의뢰 | 공단 접수자: 파일보완 / 수동파일검증 / 검토자지정 |
| `SL` | 보완요청 | 기초 수정화면. 복귀 대상은 이력의 직전 상태 |
| `AC` | 초안생성 | AI 진행 표시. **30분** 후 `TE` Fallback |
| `TE` | 기술검토 | 웹기안기·검토완료·보완. 결재선 1차 |
| `TP` | 기술검토 회신 | 환경청 → 광역 `RM` |
| `RM` | 재원협의결과회신 | 광역 → 기초 `RB` |
| `RB` | 지자체회신 | 기초 실무협의서 관리 / 조치완료 |
| `RC` | 조치결과서 제출 | 광역 → 환경청 `RA` |
| `RA` | 조치결과서제출(환경청) | 환경청 2차 의뢰 `AV` |
| `AV` | 금액검증 | AI 진행 표시. **30분** 후 `EV` Fallback |
| `EV` | 조치결과검토의뢰 | 공단 접수자: 2차 결재선 검토자지정 → `RK` |
| `RK` | 조치결과서검토 | 웹기안기·금액검증 재실행·보완. 결재선 2차 |
| `RR` | 조치결과서검토 회신 | 환경청 → 광역 `EP` |
| `EP` | 검토결과 회신 | 광역 → 기초 `TA` |
| `TA` | 검토결과 회신(종료) | 기초 조회만. **고시(**`TB`**) 아님** |
코드 의미·구버전 대조·보완 루프·AI Fallback은 [ref-process-status.md](./ref-process-status.md).
---
## 5. 프론트가 지켜야 할 핵심 규칙
1. **상태 SSOT는** `FNRSSTTS`. 버튼 가시성은 `(dtlInstCd, prgrsSttsComCd, atrzSeComCd)`로만 계산한다. 공단은 결재선 구분(`ATRZ01` 접수자, `ATRZ02` 검토자, `ATRZ03` 승인자, `ATRZ99` 최종승인자)을 추가로 본다.
2. **AI 콜백 API는 화면에서 호출하지 않는다.** `POST /dcrag/documents/verify-result`, `POST /dcrag/draft/complete`, `POST /dcrag/cost/verify-result`는 서버 전용이다.
3. **초안생성 1차 트리거는 검토자지정이다.** `POST /elapr-dmnd` (`ER → AC`). 예전처럼 `POST /pwrk-cnsltn-mng`로 초안을 시작하지 않는다.
4. **기술검토 의뢰는** `LE → FV` **전용 API** `POST /fnrs-cnsltn-mng/{docNo}/request-examine`. 현재 코드의 `updatePrgrsStts(ER)` 직행은 폐기.
5. **수동 AI 재실행은 상태를 바꾸지 않는다.**
- `ER` 파일검증 → `POST /dcrag-to/documents/transfer`
- `TE` 초안생성 → `POST /dcrag-to/documents/draft/generate` (타임아웃·실패 시에만)
- `RK` 금액검증 → `POST /dcrag-to/documents/cost/verify`
6. **진행상태만 바꾸는 버튼은** `POST /fnrs-cnsltn-mng/doc/updatePrgrsStts`**를 재사용한다.** (`ST→LE`, `RM→RB`, `RC→RA`, `RR→EP`, `EP→TA`)
7. **보완요청/보완완료는 공통 API** `reqsupplement` / `submitsupplement`. 복귀 상태(`ER`/`TE`/`RK`)는 서버가 이력에서 결정한다. 프론트는 직전 상태를 추측해 보내지 않는다.
8. **결재 승인/회신은** `PUT /elapr-prgrs-stts/{id}/process` **하나**. 1차 최종승인자 → `TP`, 2차 → `RR`은 서버가 Feign으로 처리한다.
9. **2차 결재선은 EV에서 수동 검토자지정**. `POST /elapr-dmnd` (`elaprCycl: 2`, `docPrgrsSttsComCd: RK`). 1차 결재선 자동 copy-line은 사용하지 않는다.
10. **웹기안기는 `webhwp/open` 응답을 axios `blob`으로 조회한 뒤 `File`로 감싸 `HwpCtrl.Open`에 전달한다.** `blob:` Object URL과 `orgnlOtptDocCn`은 쓰지 않는다.
---
## 6. 화면 데이터 흐름
재원협의 **목록 → 상세/작성**이 본체다. 실무협의 목록은 공단 검토 보조 화면이다.
```
BEGIN
CALL fnrsCnsltnMngService.findPage(...)
ON 행 CLICK → /fnrs-cnsltn/{fnrsCnsltnDocNo}
CALL findById(fnrsCnsltnDocNo)
COMPUTE buttons = getButtonConfigs(dtlInstCd, prgrsSttsComCd, atrzSeComCdList)
// ----- 기초 ROLE_USER05 -----
ON [저장] (빈값/SV) → saveApply(prgrsSttsComCd: SV)
ON [신청완료] (SV) → saveApply(prgrsSttsComCd: ST)
ON [삭제] (SV) → remove
ON [파일보완완료] (SL←ER) → submitsupplement
ON [보완완료] (SL←TE|RK) → submitsupplement
ON [실무협의서 관리] (RB) → GET webhwp/open (blob) → HwpCtrl.Open(File)
ON [조치완료] (RB) → submit-actn-result { isSubmit: true }
ON [결과회신] (TA) → API 없음 (조회)
// ----- 광역 ROLE_USER04 -----
ON [환경청제출] (ST) → updatePrgrsStts(LE)
ON [지자체회신] (RM) → updatePrgrsStts(RB)
ON [조치결과서제출] (RC) → updatePrgrsStts(RA)
ON [결과회신] (EP) → updatePrgrsStts(TA)
// ----- 환경청 ROLE_USER03 -----
ON [결과회신] (LE 자체종결) → tech-review → EP
ON [기술검토 의뢰] (LE) → request-examine → FV → START POLL FV
ON [재원협의 결과 회신] (TP) → tech-review → RM
ON [실무협의서 관리] (RA) → GET webhwp/open (blob) → HwpCtrl.Open(File) (저장 불가)
ON [조치결과 검토의뢰] (RA) → request-actn-review → AV → START POLL AV
ON [결과회신] (RR) → updatePrgrsStts(EP)
// ----- 공단 ROLE_USER01 -----
ON [파일보완] (ER, ATRZ01) → GET file-vrfc-results → 팝업 → reqsupplement
ON [파일검증] (ER, ATRZ01) → dcrag-to transfer (상태 유지)
ON [검토자지정] (ER, ATRZ01) → POST elapr-dmnd (docPrgrsSttsComCd: AC) → START POLL AC
ON [검토자지정] (EV, ATRZ01) → POST elapr-dmnd (elaprCycl: 2, docPrgrsSttsComCd: RK)
ON [실무협의서관리] (TE|RK) → GET webhwp/open (blob) → HwpCtrl.Open(File)
ON [실무협의서생성] (TE, 실패/타임아웃) → dcrag-to draft/generate
ON [검토결과조회] (TE) → pwrk-cnsltn-mng findById | findByUpDocNo
ON [저장] (TE|RK) → pwrk-cnsltn-mng save-apply
ON [검토완료] → elapr-prgrs-stts process (상신)
ON [보완요청] (TE|RK) → 보완 팝업 → reqsupplement
ON [금액검증] (RK) → dcrag-to cost/verify
ON [결재(승인)] → process (중간)
ON [결재(회신)] → process (최종 → TP 또는 RR)
// ----- AI 진행 중 (FV / AC / AV) -----
POLL 상태 또는 draft status. 30분 안내. 종료 후 상세 invalidate
END
```
38행 버튼 표는 [ref-screen-buttons.md](./ref-screen-buttons.md). API 상세는 [ref-endpoint.md](./ref-endpoint.md).
---
## 7. 버튼 → API 축약
프론트가 치는 것만. 콜백·스케줄러는 제외.
| 주체 | 버튼 | From → To | Method & URI |
| --------- | -------------------------- | ----------------------------- | ---------------------------------------------------------------------------------- |
| 기초 | 저장 / 신청완료 | →`SV` / `SV→ST` | `POST /fnrs-cnsltn-mng/save-apply` |
| 기초 | 삭제 | `SV` | `DELETE /fnrs-cnsltn-mng/{docNo}` |
| 광역 | 환경청제출 | `ST→LE` | `POST /fnrs-cnsltn-mng/doc/updatePrgrsStts` |
| 환경청 | 결과회신(자체종결) | `LE→EP` | `POST /fnrs-cnsltn-mng/{docNo}/tech-review` |
| 환경청 | 기술검토 의뢰 | `LE→FV` | **신규** `POST /fnrs-cnsltn-mng/{docNo}/request-examine` |
| 공단 접수 | 파일보완 팝업 | `ER` | `GET /fnrs-cnsltn-mng/{docNo}/file-vrfc-results` |
| 공단 접수 | 파일보완요청 | `ER→SL` | `POST /fnrs-cnsltn-mng/{docNo}/reqsupplement` |
| 공단 접수 | 파일검증 재실행 | `ER` 유지 | `POST /dcrag-to/documents/transfer` |
| 공단 접수 | 검토자지정 | `ER→AC` | `POST /elapr-dmnd` (`elaprCycl: 1`) |
| 공단 접수 | 검토자지정 | `EV→RK` | `POST /elapr-dmnd` (`elaprCycl: 2`) |
| 공단 검토 | 실무협의서관리 | `TE`/`RK` | `GET .../pwrk-cnsltn-doc-crt-rslt/{uuid}/webhwp/open` |
| 공단 검토 | 실무협의서생성 | `TE` 유지 | `POST /dcrag-to/documents/draft/generate` |
| 공단 검토 | 저장 | `TE`/`RK` | `POST /pwrk-cnsltn-mng/save-apply` |
| 공단 | 검토완료 / 결재(승인·회신) | `TE` 유지 / `TE→TP` / `RK→RR` | `PUT /elapr-prgrs-stts/{id}/process` |
| 공단 검토 | 보완요청 | `TE→SL` / `RK→SL` | `POST /fnrs-cnsltn-mng/{docNo}/reqsupplement` |
| 기초 | 파일보완완료 / 보완완료 | `SL→ER` / `TE` / `RK` | `POST /fnrs-cnsltn-mng/{docNo}/submitsupplement` |
| 환경청 | 재원협의 결과 회신 | `TP→RM` | `POST /fnrs-cnsltn-mng/{docNo}/tech-review` |
| 광역 | 지자체회신 | `RM→RB` | `updatePrgrsStts` |
| 기초 | 실무협의서 관리 | `RB` 유지 | `GET .../pwrk-cnsltn-doc-crt-rslt/{uuid}/webhwp/open` |
| 기초 | 조치완료 | `RB→RA` | **신규** `POST /pwrk-cnsltn-mng/{pwrkDocNo}/submit-actn-result` (`isSubmit: true`) |
| 광역 | 조치결과서제출 | `RC→RA` | `updatePrgrsStts` |
| 환경청 | 실무협의서 관리 | `RA` 유지 (조회) | `GET .../pwrk-cnsltn-doc-crt-rslt/{uuid}/webhwp/open` |
| 환경청 | 조치결과 검토의뢰 | `RA→AV` | **신규** `POST /fnrs-cnsltn-mng/{docNo}/request-actn-review` |
| 공단 검토 | 금액검증 재실행 | `RK` 유지 | `POST /dcrag-to/documents/cost/verify` |
| 환경청 | 결과회신 | `RR→EP` | `updatePrgrsStts` |
| 광역 | 결과회신 | `EP→TA` | `updatePrgrsStts` |
| 웹기안기 | 저장 | 버전 +1 | `POST /pwrk-cnsltn-doc-crt-rslt` multipart |
| 공통 | 다운로드 | — | `GET /pwrk-cnsltn-doc-crt-rslt/{uuid}/download` |
신규 4개: `request-examine`, `submit-actn-result`, `request-actn-review`, EV 2차 `elapr-dmnd`.
---
+334
View File
@@ -0,0 +1,334 @@
# 레퍼런스 — 상태별 Endpoint
> 상위: [개발가이드.md](./개발가이드.md)
> SoT: `05.재원협의_상태별_endpoint_설계.md` (v1.0, 2026-09-03)
> Gateway: `/as-service`, `/as-elapr-service`
> Auth: `Authorization`, `X-Site-Id`, `token-id` (기존 `client` 인터셉터)
> 날짜: `yyyy-MM-dd HH:mm:ss`
프론트가 치는 API만 상세히 적는다. AI 콜백·스케줄러·Feign은 [§6 호출 금지](#6-프론트-호출-금지).
---
## 1. 총괄 매트릭스 (38 + WebHWP)
| 순번 | 버튼 | From → To | 서비스 | Method & URI | 구분 |
| :---: | --- | --- | --- | --- | --- |
| 1 | 저장 | →`SV` | as | `POST /api/v1/fnrs-cnsltn-mng/save-apply` | 기존 |
| 2 | 신청완료 | `SV→ST` | as | 동일 (`prgrsSttsComCd=ST`) | 기존 |
| 3 | 삭제 | `SV` | as | `DELETE /api/v1/fnrs-cnsltn-mng/{docNo}` | 기존 |
| 4 | 환경청제출 | `ST→LE` | as | `POST /api/v1/fnrs-cnsltn-mng/doc/updatePrgrsStts` | 기존 |
| 5 | 결과회신(자체종결) | `LE→EP` | as | `POST /api/v1/fnrs-cnsltn-mng/{docNo}/tech-review` | 기존 |
| 6 | 기술검토 의뢰 | `LE→FV` | as | `POST /api/v1/fnrs-cnsltn-mng/{docNo}/request-examine` | **신규** |
| 7 | 파일검증 콜백 | `FV→ER` | as | `POST /api/v1/dcrag/documents/verify-result` | 서버 |
| 8 | 파일보완 팝업 | `ER` | as | `GET /api/v1/fnrs-cnsltn-mng/{docNo}/file-vrfc-results` | 기존 |
| 9 | 파일보완요청 | `ER→SL` | as | `POST /api/v1/fnrs-cnsltn-mng/{docNo}/reqsupplement` | 기존 |
| 10 | 파일검증 재실행 | `ER` | as | `POST /api/v1/dcrag-to/documents/transfer` | 기존 |
| 11 | 검토자지정 | `ER→AC` | elapr | `POST /api/v1/elapr-dmnd` | 기존 |
| 12 | 초안 콜백 | `AC→TE` | as | `POST /api/v1/dcrag/draft/complete` | 서버 |
| 13 | 실무협의서관리 | `TE` | as | `GET /api/v1/pwrk-cnsltn-mng/updocno/{upDocNo}` | 기존 |
| 14 | 검토결과조회 | `TE` | as | `GET /api/v1/pwrk-cnsltn-mng/{pwrkDocNo}` | 기존 |
| 15 | 실무협의서생성 | `TE` | as | `POST /api/v1/dcrag-to/documents/draft/generate` | 기존 |
| 16 | 저장(실무) | `TE` | as | `POST /api/v1/pwrk-cnsltn-mng/save-apply` | 기존 |
| 17 | 검토완료 | `TE` | elapr | `PUT /api/v1/elapr-prgrs-stts/{id}/process` | 기존 |
| 18 | 보완요청 | `TE→SL` | as | `POST .../reqsupplement` | 기존 |
| 19 | 결재(승인) | `TE` | elapr | `PUT .../process` | 기존 |
| 20 | 결재(회신) | `TE→TP` | elapr | `PUT .../process` | 기존 |
| 21 | 파일보완완료 | `SL→ER` | as | `POST .../submitsupplement` | 기존 |
| 22 | 보완완료(기술) | `SL→TE` | as | 동일 | 기존 |
| 23 | 보완완료(조치) | `SL→RK` | as | 동일 | 기존 |
| 24 | 재원협의 결과 회신 | `TP→RM` | as | `POST .../{docNo}/tech-review` | 기존 |
| 25 | 지자체회신 | `RM→RB` | as | `POST .../doc/updatePrgrsStts` | 기존 |
| 26 | 실무협의서 관리 | `RB` | as | `GET .../pwrk-cnsltn-doc-crt-rslt/{uuid}/webhwp/open` | 기존 |
| 27 | 조치완료 | `RB→RA` | as | `POST /api/v1/pwrk-cnsltn-mng/{pwrkDocNo}/submit-actn-result` (`isSubmit=true`) | **신규** |
| 28 | 조치결과서제출 | `RC→RA` | as | `updatePrgrsStts` | 기존 |
| 29 | 조치결과 검토의뢰 | `RA→AV` | as | `POST /api/v1/fnrs-cnsltn-mng/{docNo}/request-actn-review` | **신규** |
| 30 | 금액검증 콜백 | `AV→EV` | as | `POST /api/v1/dcrag/cost/verify-result` | 서버 |
| 30a | 검토자지정(2차) | `EV→RK` | elapr | `POST /api/v1/elapr-dmnd` (`elaprCycl=2`) | 기존 |
| 31 | 실무협의서관리 | `RK` | as | `GET /pwrk-cnsltn-mng/{pwrkDocNo}` | 기존 |
| 32 | 금액검증 재실행 | `RK` | as | `POST /api/v1/dcrag-to/documents/cost/verify` | 기존 |
| 33 | 보완요청(금액) | `RK→SL` | as | `reqsupplement` | 기존 |
| 34 | 결재(승인) | `RK` | elapr | `process` | 기존 |
| 35 | 결재(회신) | `RK→RR` | elapr | `process` | 기존 |
| 36 | 결과회신 | `RR→EP` | as | `updatePrgrsStts` | 기존 |
| 37 | 결과회신 | `EP→TA` | as | `updatePrgrsStts` | 기존 |
| 38 | 결과회신(확인) | `TA` | — | 없음 | — |
| W1 | 웹기안기 Open | — | as | `GET /api/v1/pwrk-cnsltn-doc-crt-rslt/{uuid}/webhwp/open` | 기존 |
| W2 | HWPX 저장 | — | as | `POST /api/v1/pwrk-cnsltn-doc-crt-rslt` multipart | 기존 |
| W3 | 다운로드 | — | as | `GET /api/v1/pwrk-cnsltn-doc-crt-rslt/{uuid}/download` | 기존 |
| E-N | 결재선 복사 | `AV→RK` 부수 | elapr | `POST /api/v1/elapr-apln/copy-line` | **신규·서버** |
조회 공통: `GET /fnrs-cnsltn-mng`, `GET /fnrs-cnsltn-mng/{docNo}`, `GET /pwrk-cnsltn-mng`, `GET /dcrag-to/documents/draft/status/{crtDmndUuid}`.
---
## 2. 공통 요청
### `DocPrgrsUpdateRequestDto`
`POST /fnrs-cnsltn-mng/doc/updatePrgrsStts` — `ST→LE`, `RM→RB`, `RC→RA`, `RR→EP`, `EP→TA`.
```json
{
"docNo": "F-20260824-0001",
"docSeComCd": "FNRS",
"prgrsSttsComCd": "LE",
"rgtrUuid": "optional-uuid",
"rmrk": "optional"
}
```
응답: `204/200` void. 프론트 엔티티: 기존 `changePrgrsSttsComCd`.
### `FnrsCnsltnApplySaveRequestDto`
`save-apply`, `tech-review`, `reqsupplement`, `submitsupplement`가 공유. 핵심은 `fnrsCnsltnMng.prgrsSttsComCd`와 중첩 리스트(`bzScaleList`, `bizCstList`, `cklstResultList`, `esntlAtchFileList`, `splmntDmndList`).
---
## 3. 단계별 FE API
### 3-1. `SV → ST → LE`
**저장 / 신청완료** — `POST /fnrs-cnsltn-mng/save-apply`
권한: `ROLE_USER05`. 임시저장은 `prgrsSttsComCd=SV`, 신청은 `ST`.
응답: `string` (문서번호). 기존 `fnrsCnsltnMngService.saveApply`.
**삭제** — `DELETE /fnrs-cnsltn-mng/{docNo}` (`SV`만).
**환경청제출** — `updatePrgrsStts` `LE`. 권한: `ROLE_USER04`.
### 3-2. `LE → FV | EP`
**자체종결** — `POST /fnrs-cnsltn-mng/{docNo}/tech-review`
Body: `FnrsCnsltnApplySaveRequestDto` (환경청 의견·체크리스트). `LE→EP`.
기존 `techReview`. 현재 화면은 `TP`에서도 이 API를 쓰며 목표 상태를 `EP`로 넣고 있다 → **`TP`는 `RM`으로 바꿔야 함**.
**기술검토 의뢰 (신규)** — `POST /fnrs-cnsltn-mng/{docNo}/request-examine`
```json
{ "prgrsSttsComCd": "FV" }
```
응답: `string` (docNo). `LE→FV` + AI 파일검증 + 30분 타이머(서버).
프론트: `fnrsCnsltnMngService.requestExamine(docNo, body)` 추가. **`updatePrgrsStts("ER")` 금지.**
### 3-3. `FV → ER → SL → ER`
파일검증 콜백은 서버. FE는 `FV`에서 상세 refetch.
**검증결과 목록** — `GET /fnrs-cnsltn-mng/{fnrsCnsltnDocNo}/file-vrfc-results`
응답: `FnrsCnsltnFileVrfcRsltResponseDto[]`
| 필드 | 설명 |
| --- | --- |
| `fileSeComCd` | `ESSENTIAL` / `ADDITIONAL` |
| `fileSeNm` | 구분명 |
| `esntlAtchFileId` | 필수첨부 ID |
| `atchFileNm` | 파일명 |
| `vrfcRsltComCd` | `PASS` / `FAIL` / `WARN` |
| `vrfcOpnn` | 검증의견 |
엔티티에 `findFileVrfcResults` 추가. 첨부 행 배지는 상세 `esntlFileList`/`etcFileList`의 `vrfcSttsComCd`, `vrfcRsltCn`, `vrfcDt`도 사용 (타입 확장 필요).
**파일보완요청** — `POST .../{docNo}/reqsupplement` (`ER→SL`).
**파일보완완료** — `POST .../{docNo}/submitsupplement` (`SL→ER`). 복귀는 서버.
**파일검증 재실행** — `POST /dcrag-to/documents/transfer`
```json
{ "fnrsCnsltnDocNo": "F-20260824-0001", "taskSeComCd": "SF", "taskSeCdNm": "하수처리시설" }
```
응답: `{ resultCode, message, requestId }`. 상태 유지. 기존 `dcRagApiToService.requestDocumentTransfer`.
### 3-4. `ER → AC → TE → TP`
**검토자지정** — `POST /elapr-dmnd`
기존 `elaprDmndService.save`.
| 필드 | 값 |
| --- | --- |
| `elaprDocNo` | 재원협의 번호 |
| `elaprCycl` | `1` |
| `docPrgrsSttsComCd` | **`AC`** (현행은 현재 상태 `ER`를 넣고 있음 → 수정) |
| `elaprPrgrsSttsList[].atrzOrdr` | 1 검토자, 2 승인자, 3 최종 |
서버가 실무협의 문서 생성 + RAG 초안 트리거 + 30분 타이머. 응답의 문서번호/`crtDmndUuid`가 있으면 폴링 키로 쓴다. 없으면 `findByUpDocNo`로 복원.
**초안 상태 폴링** — `GET /dcrag-to/documents/draft/status/{crtDmndUuid}`
기존 `getDraftGenerationStatus`. 주기 5~10초. 종료 `COMPLETED`/`FAILED`.
**수동 초안생성** — `POST /dcrag-to/documents/draft/generate` (`TE` 유지)
```json
{
"fnrsCnsltnDocNo": "F-...",
"pwrkCnsltnDocNo": "P-...",
"crtDmndUuid": "optional"
}
```
202 + `DcRagDraftGenerateResponseDto` (`crtDmndUuid`, `crtSttsComCd`, `estimatedMinutes`).
v1.0 가이드의 “이 API를 프론트가 치면 안 된다”는 **폐기**. 1차 트리거는 검토자지정이고, 이 API는 Fallback 전용.
**실무 저장** — `POST /pwrk-cnsltn-mng/save-apply` → `string`.
**보완요청/완료** — fnrs `reqsupplement` / `submitsupplement`.
**결재** — `PUT /elapr-prgrs-stts/{elaprPrgrsId}/process`
```json
{
"atrzPrgrsSttsComCd": "ATRZ_PRGRS_003",
"atrzOpnnCn": "의견",
"finalApprovalYn": false
}
```
반려: `ATRZ_PRGRS_004`. 기존 `elaprPrgrsSttsService.process`. 최종 시 서버가 `TP`/`RR` 전이.
### 3-5. `TP → RM → RB → RA`
**환경청 1차 회신** — `tech-review` (`TP→RM`).
**광역 지자체회신** — `updatePrgrsStts` `RB`.
**조치완료 (신규)** — `POST /pwrk-cnsltn-mng/{pwrkDocNo}/submit-actn-result`
```json
{
"isSubmit": true,
"actnList": [
{
"rvwOpnnSn": 1,
"actnRsltCn": "수용 — 관로 연장 재산정",
"atchFileGroupId": "optional"
}
],
"totActnSummary": "제출 시 권장",
"muniDocNo": "제출 시 권장"
}
```
- `isSubmit: false` → `RB` 유지 (API만 존재, **화면 버튼 없음**. HWP·첨부는 실무협의서 관리 다이얼로그에서 저장)
- `isSubmit: true` → `RB→RA` (조치완료)
응답: `string` (pwrkDocNo). DTO명 `PwrkActnResultSubmitDto`.
### 3-6. `RA → AV → EV → RK`
**2차 의뢰 (신규)** — `POST /fnrs-cnsltn-mng/{docNo}/request-actn-review`
```json
{
"specialRqstCn": "중점 검증 요청 (선택)"
}
```
`RA→AV`. 응답: `string` (docNo). DTO명 `EnvoActnReviewReqDto`. `specialRqstCn`만 선택.
금액검증 콜백은 서버 → `AV→EV`.
**2차 검토자지정** — `POST /elapr-dmnd` (`elaprCycl: 2`, `docPrgrsSttsComCd: RK`). 1차 결재선(`findDocPrgrsByDocNo`, cycl=1)을 시드로 편집.
**금액검증 재실행** — `POST /dcrag-to/documents/cost/verify`
```json
{
"fnrsCnsltnDocNo": "F-...",
"pwrkCnsltnDocNo": "P-..."
}
```
202. 기존 `requestCostVerification`. `RK` 유지.
### 3-7. `RK → RR | SL`
보완은 3-4와 동일 API. 팝업만 금액 체크.
결재 `process` 2차 최종 → 서버가 `RR`.
### 3-8. `RR → EP → TA`
둘 다 `updatePrgrsStts`. `TA` 확인은 API 없음.
---
## 4. 웹기안기
설계서 기준 **기존**. 구현 시 컨트롤러 존재 여부를 확인한다.
| ID | 용도 | URI | 응답 |
| --- | --- | --- | --- |
| W1 | 웹기안기 Open | `GET .../pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/webhwp/open` | `inline` HWPX blob → `HwpCtrl.Open(File)` |
| W2 | 편집 저장 | `POST .../pwrk-cnsltn-doc-crt-rslt` multipart `file` + `crtDmndUuid` | 201, `verSn+1` |
| W3 | PC 저장 | `GET .../{crtDmndUuid}/download` | `attachment` blob |
버전 지정: `GET .../{crtDmndUuid}/{verSn}/webhwp/open`, `.../{verSn}/download`.
`/{uuid}/latest`, `/{uuid}/download`가 `/{uuid}/{verSn}`보다 우선. path를 섞지 말 것.
다운로드:
- axios `responseType: "blob"`
- 파일명: `Content-Disposition` `filename*=UTF-8''...` (`parseFilenameFromResponse`)
- 서버 파일명: `재원협의 실무협의서({bizNm})_v{verSn}.hwp`
- Content-Type은 `application/x-hwp` 또는 `application/haansofthwpx`일 수 있음. 타입에 의존하지 말 것
Open은 axios `responseType: "blob"`으로 인증된 API를 호출하고, 응답 Blob을 HWPX `File`로 감싸 `HwpCtrl.Open`에 전달한다. `blob:` Object URL은 사용하지 않는다.
multipart `Content-Type` 헤더를 수동 지정하지 않는다 (boundary). JSON `saveJson`은 UI에서 쓰지 않는다.
---
## 5. 전자결재 (`as-elapr-service`)
| API | URI | FE |
| --- | --- | --- |
| 검토자지정·1차 결재요청 | `POST /elapr-dmnd` | O |
| 결재 처리 | `PUT /elapr-prgrs-stts/{id}/process` | O |
| 결재선 복사 | `POST /elapr-apln/copy-line` | **X (서버)** |
`copy-line` body (참고만):
```json
{
"docNo": "P-20260827-0001",
"sourceElaprCycl": 1,
"targetElaprCycl": 2,
"rgtrUuid": "system-uuid"
}
```
`RK`에서 결재현황은 `findDocPrgrsByDocNo`(전체) 조회 후 `elaprCycl`로 1차/2차 탭 표시.
---
## 6. 프론트 호출 금지
| URI | 이유 |
| --- | --- |
| `POST /dcrag/documents/verify-result` | AI→AS 콜백 |
| `POST /dcrag/draft/complete` | AI→AS 콜백 |
| `POST /dcrag/cost/verify-result` | AI→AS 콜백. 여기서 copy-line도 호출 |
| `POST /elapr-apln/copy-line` | 콜백 Feign |
| `POST /pwrk-cnsltn-mng` (`save` 초안 트리거) | v3.0 1차 트리거는 `elapr-dmnd`. 컨트롤러가 남아 있어도 화면에서 쓰지 않음 |
`POST /dcrag-to/documents/draft/generate`는 **Fallback에서만** 허용.
---
## 7. Gap Analysis (프론트 엔티티)
### 이미 있는 것
`fnrsCnsltnMngService`: `findPage`, `findById`, `save`, `saveApply`, `update`, `techReview`, `submitSupplement`, `submitReqSupplement`, `remove`, `changePrgrsSttsComCd`
`dcRagApiToService`: `requestDocumentTransfer`, `requestDraftGeneration`, `getDraftGenerationStatus`, `requestCostVerification`
`elaprDmndService.save`, `elaprPrgrsSttsService.process`
### 추가할 것
| 메서드 | URI |
| --- | --- |
| `requestExamine` | `POST /fnrs-cnsltn-mng/{id}/request-examine` |
| `findFileVrfcResults` | `GET /fnrs-cnsltn-mng/{id}/file-vrfc-results` |
| `requestActnReview` | `POST /fnrs-cnsltn-mng/{id}/request-actn-review` |
| `pwrkCnsltnMngService.*` | 슬라이스 신규 |
| `submitActnResult` | `POST /pwrk-cnsltn-mng/{id}/submit-actn-result` |
| `pwrkCnsltnDocCrtRsltService.*` | 슬라이스 신규 |
`copyLine`은 프론트 서비스에 넣지 않는다.
@@ -0,0 +1,350 @@
# 레퍼런스 — 프론트 엔티티·DTO
> 상위: [개발가이드.md](./개발가이드.md)
> 엔티티 규칙: `frontend/.cursor/skills/backend-controller-to-entity`
---
## 1. ENDPOINT
`apps/as/src/shared/config/api.ts`
```ts
AS_SERVICE: {
// 기존
FNRS_CNSLTN_MNG: "/as-service/api/v1/fnrs-cnsltn-mng",
DCRAG_TO: "/as-service/api/v1/dcrag-to",
// 추가
PWRK_CNSLTN_MNG: "/as-service/api/v1/pwrk-cnsltn-mng",
PWRK_CNSLTN_DOC_CRT_RSLT: "/as-service/api/v1/pwrk-cnsltn-doc-crt-rslt",
}
AS_ELAPR_SERVICE: {
ELAPR_TMPLT: "/as-elapr-service/api/v1/elapr-tmplt",
ELAPR_APLN: "/as-elapr-service/api/v1/elapr-apln",
ELAPR_DMND: "/as-elapr-service/api/v1/elapr-dmnd",
ELAPR_PRGRS_STTS: "/as-elapr-service/api/v1/elapr-prgrs-stts",
}
```
`copy-line`은 `ELAPR_APLN` 하위이나 FE에서 호출하지 않음.
---
## 2. 슬라이스 배치
```
entities/fnrs-cnsltn/ # 확장
entities/dcrag-to/ # 구현됨
entities/elapr/ # 구현됨
entities/pwrk-cnsltn-mng/ # 신규
entities/pwrk-cnsltn-doc-crt-rslt/ # 신규
```
```
entities/pwrk-cnsltn-mng/
├── model/
│ ├── pwrk-cnsltn-mng-interface.ts
│ └── pwrk-cnsltn-mng-payload-schema.ts
├── api/
│ ├── pwrk-cnsltn-mng-service.ts
│ └── queries.ts
└── index.ts
```
`submit-actn-result` Request는 zod SSOT (`PwrkActnResultSubmitDto`).
`request-examine` / `request-actn-review`도 fnrs payload-schema에 추가.
라우트:
| 화면 | 경로 |
| --- | --- |
| 재원협의 목록/상세/작성 | 기존 `app/(main)/(with-base-layout)/fnrs-cnsltn/...` |
| 실무협의 목록 | `.../pwrk-cnsltn/page.tsx` (선택) |
| 실무협의 상세 | `.../pwrk-cnsltn/[id]/page.tsx` (선택) |
| 페이지 UI | `pages/fnrs-cnsltn/`, `pages/pwrk-cnsltn/` |
---
## 3. Service 시그니처
`fnrsCnsltnMngService`에 추가:
```ts
requestExamine(docNo: string, body: FnrsExamineRequestDto): Promise<string>;
findFileVrfcResults(fnrsCnsltnDocNo: string): Promise<FnrsCnsltnFileVrfcRsltResponseDto[]>;
requestActnReview(docNo: string, body: EnvoActnReviewReqDto): Promise<string>;
```
실무협의:
```ts
export interface PwrkCnsltnMngService {
findPage(searchDto: PwrkCnsltnMngSearchDto): Promise<PageResponse<PwrkCnsltnMngResponseDto>>;
findById(pwrkCnsltnDocNo: string): Promise<PwrkCnsltnMngDetResponseDto>;
findByUpDocNo(upDocNo: string): Promise<PwrkCnsltnMngDetResponseDto>;
saveApply(body: PwrkCnsltnApplySaveRequestDto): Promise<string>;
submitActnResult(
pwrkCnsltnDocNo: string,
body: PwrkActnResultSubmitDto,
): Promise<string>;
update(pwrkCnsltnDocNo: string, body: PwrkCnsltnMngUpdateRequestDto): Promise<string>;
remove(pwrkCnsltnDocNo: string): Promise<void>;
changePrgrsSttsComCd(body: DocPrgrsUpdateRequestDto): Promise<void>;
}
```
v1.0의 `save(): PwrkCnsltnMngSaveResponseDto`(초안 트리거)는 화면에서 사용하지 않는다. 컨트롤러가 남아 있으면 엔티티에 넣더라도 UI 연결 금지.
생성결과:
```ts
export interface PwrkCnsltnDocCrtRsltService {
findPage(
searchDto: PwrkCnsltnDocCrtRsltSearchDto,
): Promise<PageResponse<PwrkCnsltnDocCrtRsltResponseDto>>;
findById(crtDmndUuid: string, verSn: number): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
findLatestByCrtDmndUuid(crtDmndUuid: string): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
openWebHwpLatest(crtDmndUuid: string): Promise<AxiosResponse<Blob>>;
download(crtDmndUuid: string, verSn: number): Promise<AxiosResponse<Blob>>;
downloadLatest(crtDmndUuid: string): Promise<AxiosResponse<Blob>>;
saveMultipart(formData: FormData): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
updateMultipart(
crtDmndUuid: string,
verSn: number,
formData: FormData,
): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
remove(crtDmndUuid: string, verSn: number): Promise<void>;
}
```
`webhwp/open`은 인증된 axios 요청으로 Blob을 조회한다:
```ts
async function openWebHwpLatest(crtDmndUuid: string) {
return client.get<Blob>(`${BASE}/${crtDmndUuid}/webhwp/open`, {
responseType: "blob",
});
}
```
응답 Blob은 HWPX `File`로 감싸 `HwpCtrl.Open(file, "HWPX", ...)`에 전달한다. `blob:` Object URL은 사용하지 않는다.
DC RAG TO (구현됨, 시그니처 유지):
```ts
requestDocumentTransfer(body: DcRagDocumentTransferRequestDto): Promise<DcRagCommonResponseDto>;
requestDraftGeneration(body: DcRagDraftGenerateRequestDto): Promise<DcRagDraftGenerateResponseDto>;
getDraftGenerationStatus(crtDmndUuid: string): Promise<DcRagDraftStatusResponseDto>;
requestCostVerification(body: DcRagCostVerifyRequestDto): Promise<DcRagCommonResponseDto>;
```
---
## 4. DTO
타입명 = 백엔드 클래스명. Save/Apply는 zod.
### 4-1. 재원협의 추가
**`FnrsExamineRequestDto`**
| 필드 | 필수 | 값 |
| --- | --- | --- |
| `prgrsSttsComCd` | N | 기본 `FV` |
**`EnvoActnReviewReqDto`**
| 필드 | 필수 |
| --- | --- |
| `specialRqstCn` | N |
**`FnrsCnsltnFileVrfcRsltResponseDto`**: [ref-endpoint.md](./ref-endpoint.md) §3-3.
첨부 검증 배지용으로 `AtflMngClientResponseDto` / `EsntlAtchFileMngTreeResponseDto`에 `vrfcDt`, `vrfcRsltCn`, `vrfcSttsComCd` 확장.
### 4-2. `PwrkActnResultSubmitDto` (zod)
| 필드 | 필수 | 비고 |
| --- | --- | --- |
| `isSubmit` | Y | `false` API만(화면 버튼 없음), `true` 조치완료(`RB→RA`) |
| `actnList` | N | 조치완료 시 선택 |
| `actnList[].rvwOpnnSn` | Y (항목 있을 때) | |
| `actnList[].actnRsltCn` | N | |
| `actnList[].atchFileGroupId` | N | |
| `totActnSummary` | N | 제출 시 권장 |
| `muniDocNo` | N | 제출 시 권장 |
| `pwrkAtchFileGroupId` | N | |
### 4-3. `PwrkCnsltnMng*`
**Search**: `pwrkCnsltnDocNo`, `upDocNo`, `upDocSeComCd`, `prgrsSttsComCd`, `admdstCd`, `envoSeComCd`, `taskSeComCd`, `bizNm`
**DetResponse 중첩**
- `cnsltnList`: 협의내역 (`rvwOpnnSn`, `rvwOpnnClsfComCd`, `cnsltnCn`, `rfltYn`, …)
- `pjtcoAjmtList`: 사업비조정
- `crtDmndList[].crtRslt`: 생성요청 + 최신 결과
- `splmntDmndList`: 보완요청
- `sttsChgHstryList`: 상태변경이력 — `SL` 복귀 라벨에 사용
- `trsmFileList`: 전송파일 (Java Det DTO 확인 후 반영)
마스터 조인: `bizNm`, `admdstCd`, `envoSeComCd`, `taskSeComCd`, `aplcntNm` 등.
`crtRslt.orgnlOtptDocCn`은 항상 `null`.
**상세 예**
```json
{
"pwrkCnsltnDocNo": "P-20260827-0001",
"upDocNo": "F-20260824-0001",
"upDocSeComCd": "FNRS",
"prgrsSttsComCd": "TE",
"cnsltnList": [],
"pjtcoAjmtList": [],
"crtDmndList": [
{
"crtDmndUuid": "d0545bef-735f-4d7b-a781-88e96e03577a",
"pwrkCnsltnDocNo": "P-20260827-0001",
"rqstrUuid": "...",
"rqstrNm": "관리자",
"crtSttsComCd": "C",
"crtRslt": {
"crtDmndUuid": "d0545bef-735f-4d7b-a781-88e96e03577a",
"verSn": 1,
"rgtrNm": "관리자",
"orgnlOtptDocCn": null,
"crtCmptnDt": "2026-08-27 10:47:01"
}
}
],
"splmntDmndList": [],
"sttsChgHstryList": []
}
```
### 4-4. `PwrkCnsltnDocCrtRslt*`
복합 PK: `crtDmndUuid` + `verSn`
**Response**
| 필드 | 타입 | 비고 |
| --- | --- | --- |
| `crtDmndUuid` | `string` | |
| `verSn` | `number` | |
| `rgtrUuid` / `rgtrNm` | `string` | |
| `orgnlOtptDocCn` | `null` | 목록/상세 미포함 |
| `crtCmptnDt` | `string` | |
**Search** (`PageableParams`): `crtDmndUuid`(이력 조회 시 필수), `pwrkCnsltnDocNo`, `verSn`, `rgtrUuid`
```http
GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt?page=0&size=10&crtDmndUuid={uuid}&sort=verSn,desc
```
**Save (zod)**: `crtDmndUuid` 필수. `rgtrUuid` 생략 시 서버 로그인 사용자. `file`은 multipart part (DTO 필드 아님).
### 4-5. DC RAG (구현됨)
```ts
interface DcRagDocumentTransferRequestDto {
fnrsCnsltnDocNo: string;
taskSeComCd?: string;
taskSeCdNm?: string;
}
interface DcRagDraftStatusResponseDto {
crtDmndUuid: string;
status: DraftStatus;
statusName?: string;
progressPercent?: number;
events?: EventInfoDto[];
startedDt?: string;
estimatedCompletionDt?: string;
errorMessage?: string | null;
}
type DraftStatus =
| "REQUESTED"
| "DOWNLOADING"
| "ANALYZING"
| "GENERATING"
| "COMPLETED"
| "FAILED";
```
`estimatedMinutes`는 초안 응답에 있을 수 있으나 UX 안내는 **30분 Fallback**을 따른다.
---
## 5. 폴링 훅
초안 (`AC` 또는 `TE`이면서 생성 중):
```ts
useQuery({
queryKey: [...dcragToKeys.draftStatus(crtDmndUuid)],
queryFn: () => dcRagApiToService.getDraftGenerationStatus(crtDmndUuid),
enabled: Boolean(crtDmndUuid) && isPolling,
refetchInterval: (query) => {
const status = query.state.data?.status;
if (status === "COMPLETED" || status === "FAILED") return false;
return 5_000;
},
});
```
`COMPLETED` 시 `fnrs-cnsltn` 상세·`pwrk-cnsltn-mng` `findByUpDocNo` invalidate.
`FV`/`AV`는 `findById`를 5~10초 refetch. `prgrsSttsComCd`가 `ER`/`RK`가 되면 중단. 시작 시각 기준 30분 경과 안내 문구.
`crtDmndUuid` 복원: `findByUpDocNo(fnrsCnsltnDocNo).crtDmndList[0]`.
---
## 6. 다운로드
```ts
const response = await client.get<Blob>(url, { responseType: "blob" });
const filename = parseFilenameFromResponse(response);
const objectUrl = window.URL.createObjectURL(response.data);
```
기존 `attachService`와 동일.
---
## 7. 실무협의 목록 컬럼 (선택 화면)
| 표시명 | Field |
| --- | --- |
| 실무협의번호 | `pwrkCnsltnDocNo` |
| 재원협의번호 | `upDocNo` |
| 진행상태 | `prgrsSttsComCdNm` |
| 사업명 | `bizNm` |
| 시도/구군 | `sido` / `gugun` |
| 신청자 | `aplcntNm` |
| 등록일 | `regDt` |
행 클릭 → `/pwrk-cnsltn/{pwrkCnsltnDocNo}` 또는 재원협의 상세로 이동 (화면설계 확정 후).
---
## 8. `elapr-dmnd` 저장 시 주의
현행 `floating-action-buttons.tsx`:
```ts
elaprCycl: data.detDto?.prgrsSttsComCd === "RK" ? 2 : 1,
docPrgrsSttsComCd: data.detDto?.prgrsSttsComCd, // ER
```
To-Be:
- 검토자지정은 **`ER`(1차)** 과 **`EV`(2차)** 에서.
- `ER`: `elaprCycl: 1`, `docPrgrsSttsComCd: "AC"`, 서식 선택 다이얼로그
- `EV`: `elaprCycl: 2`, `docPrgrsSttsComCd: "RK"`, 1차 결재선 시드 다이얼로그(서식 없음)
`RK` / ATRZ02 FAB: `findDocPrgrsByDocNo` 전체 조회 후 pending은 `elaprCycl === 2`만 사용. 검토완료 후 `ElaprApprovalDialog`에 `elaprCycl={2}`. 보완요청은 TE와 동일 `SplmntDmndDialog`.
@@ -0,0 +1,246 @@
# 레퍼런스 — 프로세스·상태 전이
> 상위: [개발가이드.md](./개발가이드.md)
> SoT: `03.재원협의_실무협의_프로세스_통합_v완결.md` (v3.0, 2026-09-03)
---
## 1. 프로세스 개요
| 항목 | 내용 |
| --- | --- |
| Process No. | PR-AS-003 |
| 메뉴 | 행정지원 > 2.2.2 재원협의 > 2.2.2.1 재원협의 관리 |
| 목적 | 기초 → 광역(경유) → 환경청(승인) → 공단(기술검토/AI) 4자 협의로 사업규모·국비·분담금을 확정 |
2-Pass:
- **Pass 1** 기술 타당성: `SV→ST→LE→FV→ER→AC→TE→TP→RM→RB`
- **Pass 2** 조치결과·금액: `RB→RA→AV→EV→RK→RR→EP→TA`
자체종결(기술검토 생략): `LE→EP→TA`
---
## 2. 4-Lane 시퀀스
```mermaid
sequenceDiagram
autonumber
actor 기초 as 기초 (ROLE_USER05)
actor 광역 as 광역 (ROLE_USER04)
actor 환경청 as 환경청 (ROLE_USER03)
actor 공단 as 공단 (ROLE_USER01)
actor AI as AI 엔진
rect rgb(240, 248, 255)
기초->>기초: [저장] SV
기초->>광역: [신청완료] ST
광역->>환경청: [환경청제출] LE
end
rect rgb(255, 250, 240)
alt 자체종결
환경청->>광역: [결과회신] LE → EP
광역->>기초: [결과회신] EP → TA
else 기술검토 의뢰
환경청->>AI: [기술검토 의뢰] LE → FV
end
end
rect rgb(245, 255, 245)
AI-->>공단: 파일검증 콜백 FV → ER
Note over 공단: 30분 미응답 시 ER + 수동 [파일검증]
alt 파일 미비
공단->>기초: [파일보완] ER → SL
기초->>공단: [파일보완완료] SL → ER
else 적격
공단->>AI: [검토자지정] ER → AC
end
end
rect rgb(255, 245, 245)
AI-->>공단: 초안 콜백 AC → TE
Note over 공단: 30분 미응답 시 TE + 수동 [실무협의서생성]
alt 설계 미비
공단->>기초: [보완요청] TE → SL
기초->>공단: [보완완료] SL → TE
else 검토 완료
공단->>공단: [검토완료] 상신, [결재(승인)] TE 유지
공단->>환경청: [결재(회신)] TE → TP
end
end
rect rgb(250, 240, 255)
환경청->>광역: [재원협의 결과 회신] TP → RM
광역->>기초: [지자체회신] RM → RB
기초->>환경청: [조치완료] RB → RA
end
rect rgb(240, 255, 255)
광역->>환경청: [조치결과서제출] RC → RA
환경청->>AI: [조치결과 검토의뢰] RA → AV
AI-->>공단: 금액검증 콜백 AV → EV
공단->>공단: [검토자지정] EV → RK
Note over 공단: EV에서 1차 결재선 시드로 2차 결재선 수동 배정
end
rect rgb(255, 255, 230)
alt 소명 미비
공단->>기초: [보완요청] RK → SL
기초->>공단: [보완완료] SL → RK
else 확정
공단->>환경청: [결재(회신)] RK → RR
end
end
rect rgb(255, 240, 240)
환경청->>광역: [결과회신] RR → EP
광역->>기초: [결과회신] EP → TA
end
```
거버넌스:
```
기초 (USER05) --ST--> 광역 (USER04) --LE--> 환경청 (USER03)
기초 <-----TA------ 광역 <-----EP/RM------ 환경청
환경청 --FV/ER--> 공단 (USER01) --TP/RR--> 환경청
```
---
## 3. Activity 정의 (14)
| No. | Activity | 주체 | From → To | FE 포인트 |
| --- | --- | --- | --- | --- |
| 1 | 재원협의 조회 | 기초 | — | 기본계획·자가점검 대조 |
| 2 | 등록 및 제출 | 기초·광역 | `SV→ST`, 광역 `LE` | `save-apply`, `updatePrgrsStts` |
| 3 | 접수 및 의뢰 | 환경청 | `LE→FV` / `EP` | 의뢰 vs 자체종결 분기 |
| 4 | AI 파일검증 | 공단·AI | `FV→ER` / `SL` | 30분 Fallback, 파일보완 팝업 |
| 5 | 자체종결 회신 | 환경청·광역 | `LE→EP→TA` | 공단 레인 없음 |
| 6 | 검토자지정·초안 | 공단·AI | `ER→AC→TE` | `elapr-dmnd`, 초안 폴링 |
| 7 | 1차 기술검토 | 공단 결재선 | `TE` / `SL` | 웹기안기, HWP 파싱은 서버 |
| 8 | 기술검토 회신 | 공단 최종 | `TE→TP` | `process` 최종 |
| 9 | 검토결과 회신 | 환경청·광역 | `TP→RM→RB` | 1차 회신. `EP`와 혼동 금지 |
| 10 | 조치결과 작성 | 기초 | `RB→RA` | `submit-actn-result` `isSubmit:true` (조치완료) |
| 11 | 조치 의뢰 | 환경청 | `RA→AV` | `request-actn-review` |
| 12 | AI 금액검증 | AI·공단 | `AV→EV` | 콜백/Fallback 후 접수자 대기 |
| 12a | 2차 검토자지정 | 공단 접수 | `EV→RK` | `elapr-dmnd` cycl=2 |
| 13 | 2차 검토·결재 | 공단 | `RK→RR` / `SL` | 2차 결재선 |
| 14 | 최종 회신 | 환경청·광역·기초 | `RR→EP→TA` | 종결 |
---
## 4. `FNRSSTTS` 상세
관리: `tb_cs_lwpo_cd` / `tb_as_fnrs_cnsltn_mng.prgrs_stts_com_cd`
| 코드 | 국문 | 영문 | 화면 주체 | 전이 |
| --- | --- | --- | --- | --- |
| `SV` | 저장 | Save | 기초 작성 | 신규/`SV` → `SV` |
| `ST` | 신청완료 | Submit to Province | 기초→광역 | `SV→ST` |
| `LE` | 환경청 검토 | Local Environment Review | 광역→환경청 | `ST→LE` |
| `FV` | 파일검증 | File Verification | AI | `LE→FV` |
| `ER` | 기술검토 의뢰 | Examine Request | 공단 접수자 | `FV→ER`, `SL→ER` |
| `SL` | 보완요청 | Supplement Loop | 공단→기초 | `ER\|TE\|RK → SL` |
| `AC` | 초안생성 | AI Auto Creation | AI | `ER→AC` |
| `TE` | 기술검토 | Technical Examine | 공단 1차 결재 | `AC→TE`, `SL→TE` |
| `TP` | 기술검토 회신 | Technical Pass to Basin | 공단→환경청 | `TE→TP` |
| `RM` | 재원협의결과회신 | Result Message to Province | 환경청→광역 | `TP→RM` |
| `RB` | 재원협의결과회신(지자체) | Result Back to Municipality | 광역→기초 | `RM→RB` |
| `RC` | 조치결과서 제출 | Result Complete | 기초→광역 | `RB→RC` |
| `RA` | 조치결과서 제출(환경청) | Result Ask to Basin | 기초→환경청 | `RB→RA` |
| `AV` | 금액검증 | Amount Verification | AI | `RA→AV` |
| `EV` | 조치결과검토의뢰 | Examine Verify | 공단 접수자 | `AV→EV` |
| `RK` | 조치결과서검토 | Result Check | 공단 2차 결재 | `EV→RK`, `SL→RK` |
| `RR` | 조치결과서검토 회신 | Result Return to Basin | 공단→환경청 | `RK→RR` |
| `EP` | 검토결과 회신 | Examine Pass to Province | 환경청→광역 | `LE→EP`(자체), `RR→EP`(최종) |
| `TA` | 검토결과 회신(종료) | Technical Approval | 광역→기초 | `EP→TA` |
### 4-1. v2.1 → v3.0 대조 (프론트 폐기 대상)
| 구간 | v2.1 | v3.0 |
| --- | --- | --- |
| 의뢰 직후 파일검증 | `ER` | **`FV` → `ER`** |
| 검토자지정 후 초안 | `EE` → `DV` | **`ER` → `AC` → `TE`** |
| 1차 내부 결재 | `TR` → `TP` | **`TE` 유지 → `TP`** |
| 환경청→광역 1차 회신 | `EP` | **`RM`** |
| 금액검증 | `RA` | **`RA` → `AV` → `RK`** |
| 공단 2차 회신 | `TP` | **`RR`** |
| 최종 완료 | `TP` / 고시 `TB` | **`TA`** (고시 없음) |
가이드 v1.0 `PWRKSTTS`(`DV`, `AR`, `TR`, `AA` 등)도 동일하게 폐기.
---
## 5. 보완(`SL`) 3루프
복귀 상태는 `tb_as_doc_stts_chg_hstry` 직전 코드. 프론트는 복귀 코드를 요청에 넣지 않고, 라벨만 이력으로 고른다.
```mermaid
stateDiagram-v2
ER --> SL: 파일보완 팝업(파일리스트)
SL --> ER: 파일보완완료
TE --> SL: 보완 검토팝업()
SL --> TE: 보완완료
RK --> SL: 보완 검토팝업(금액)
SL --> RK: 보완완료
```
| 루프 | 팝업 | 요청 | 제출 버튼 |
| --- | --- | --- | --- |
| 파일 무결성 | 파일리스트 + 검증결과 | 공단 접수자 `ER` | [파일보완완료] |
| 1차 기술 | 설계 미달·사유 | 공단 검토자 `TE` | [보완완료] |
| 2차 금액 | 공종별 금액 체크 | 공단 검토자 `RK` | [보완완료] |
---
## 6. AI 3파이프라인·30분 Fallback
| ID | 단계 | 진입 → 완료 | 프론트 진입 | Fallback | 수동 재실행 |
| --- | --- | --- | --- | --- | --- |
| AI-1 | 파일 무결성 | `FV→ER` | 환경청 [기술검토 의뢰] | 30분 → `ER` + [파일검증] | `POST /dcrag-to/documents/transfer` |
| AI-2 | 초안 생성 | `AC→TE` | 공단 [검토자지정] | 30분 → `TE` + [실무협의서생성] | `POST /dcrag-to/documents/draft/generate` |
| AI-3 | 금액 검증 | `AV→EV` | 환경청 [조치결과 검토의뢰] | 30분 → `EV` + 수동 [금액검증] | `POST /dcrag-to/documents/cost/verify` |
- 예상 소요 안내는 **30분** (v1.0의 10분은 폐기).
- 콜백 수신·스케줄러 전이는 서버. FE는 상세 refetch / draft status 폴링만.
- `AC→TE` 성공 시에만 [실무협의서관리]·[검토결과조회] 활성. 실패·타임아웃이면 [실무협의서생성].
초안 폴링 이벤트 예:
| `eventType` | `eventMessage` |
| --- | --- |
| `REQUESTED` | 요청을 접수했습니다. |
| `DOWNLOADING` | 신청서를 수신하는 중입니다. |
| `ANALYZING` | 자료 준비가 완료되었습니다. |
| `GENERATING` | 문서 저장이 완료되었습니다. |
| `COMPLETED` | 문서 생성이 완료되었습니다. |
| `FAILED` | `errorMessage` 표시 |
종료: `status === "COMPLETED" | "FAILED"` 또는 `events`에 해당 `eventType`.
---
## 7. 2차 결재선 (수동 검토자지정)
1. `TE`에서 1차 결재선(`elaprCycl = 1`) 완료.
2. 금액검증 콜백/Fallback → `AV→EV`.
3. 공단 접수자가 `EV`에서 [검토자지정]: 1차 결재선을 시드로 편집 후 `POST /elapr-dmnd` (`elaprCycl: 2`) → `RK`.
4. `copy-line` 자동 복사는 사용하지 않는다.
결재 `process`:
- 상신·중간 승인: 문서 상태 `TE` 또는 `RK` 유지.
- 1차 최종: 서버가 `TE→TP`.
- 2차 최종: 서버가 `RK→RR`.
- 반려: 직전 결재자 복귀. 문서 상태 유지.
---
## 8. 웹기안기
- `TE`/`RK` [실무협의서관리] → `webhwp/open` Blob 조회 후 팝업에서 `HwpCtrl.Open(File)`.
- [검토완료] 시 HWP 파싱·DB 등재는 **서버**. FE는 결재 `process`만.
- 편집본 저장: `POST /pwrk-cnsltn-doc-crt-rslt` multipart. `verSn`은 클라이언트가 보내지 않음.
@@ -0,0 +1,181 @@
# 레퍼런스 — 화면·버튼 매트릭스
> 상위: [개발가이드.md](./개발가이드.md)
> SoT: `03` §4 화면 및 버튼별 상세 상태전이 명세서 (Row 4~42, 38행)
구현 위치: `pages/fnrs-cnsltn/lib/button-config.ts` + `ui/components/floating-action-buttons.tsx`
---
## 1. 화면 구분
| 화면 | 소유 | 라우트 (현행) | 비고 |
| --- | --- | --- | --- |
| 작성화면 | 기초 `ROLE_USER05` | `/fnrs-cnsltn/new`, `/fnrs-cnsltn/{id}/edit` | `SV` |
| 수정화면 | 기초 | `/fnrs-cnsltn/{id}/edit` | `SL` 보완 |
| 상세화면 | 광역·환경청·공단·기초(`RB`/`TA`) | `/fnrs-cnsltn/{id}` | 조회 + 액션 버튼 |
| 자동화 | AI | 버튼 없음 | `FV`/`AC`/`AV` 진행 패널 |
공단 상세는 결재선 구분으로 같은 상태라도 버튼이 다르다.
| `atrzSeComCd` | 역할 |
| --- | --- |
| `ATRZ01` | 접수자 |
| `ATRZ02` | 검토자 |
| `ATRZ03` | 승인자 |
| `ATRZ99` | 최종승인자 |
---
## 2. 38행 매트릭스
API 컬럼은 [ref-endpoint.md](./ref-endpoint.md)와 동일.
| 순번 | 화면 | 권한 | 결재선 | 상태 | 버튼 | 전이 | 다음 담당 | FE 동작 |
| :---: | --- | --- | --- | --- | --- | --- | --- | --- |
| 1 | 작성 | USER05 | - | 빈값, `SV` | 저장 | `SV` | 본인 | `save-apply` `SV` |
| 2 | 작성 | USER05 | - | `SV` | 신청완료 | `ST` | 광역 | `save-apply` `ST` |
| 3 | 작성 | USER05 | - | `SV` | 삭제 | 삭제 | — | `DELETE` |
| 4 | 상세 | USER04 | - | `ST` | 환경청제출 | `LE` | 환경청 | `updatePrgrsStts` |
| 5 | 상세 | USER03 | - | `LE` | 결과회신 | `EP` | 광역 | `tech-review` 자체종결 |
| 6 | 상세 | USER03 | - | `LE` | 기술검토 의뢰 | `FV` | 공단 접수 | **`request-examine`** |
| 7 | 자동화 | AI | - | `FV` | (파일검증) | `ER` | 공단 접수 | FE 호출 없음. 진행 패널 |
| 8 | 상세 | USER01 | 접수자 | `ER` | 파일보완 | `SL` | 기초 | 팝업(파일리스트) → `reqsupplement` |
| 9 | 상세 | USER01 | 접수자 | `ER` | 파일검증 | `ER` | 본인 | `dcrag-to/transfer` 재실행 |
| 10 | 상세 | USER01 | 접수자 | `ER` | 검토자지정 | `AC` | 다음 결재자 | `POST /elapr-dmnd` |
| 11 | 자동화 | AI | - | `AC` | (초안생성) | `TE` | 공단 검토 | FE 호출 없음. 폴링 |
| 12 | 상세 | USER01 | 검토자 | `TE` | 실무협의서관리 | 유지 | — | 웹기안기 Open. **초안 성공 시만** |
| 13 | 상세 | USER01 | 검토자 | `TE` | 검토결과조회 | 유지 | — | 실무협의 상세 |
| 14 | 상세 | USER01 | 검토자 | `TE` | 실무협의서생성 | 유지 | — | **실패/타임아웃/초안 없음** 시 |
| 15 | 상세 | USER01 | 검토자 | `TE` | 저장 | `TE` | — | `pwrk save-apply` |
| 16 | 상세 | USER01 | 검토자 | `TE` | 검토완료 | `TE` | 다음 결재자 | `elapr process` 상신. 반려 시 직전 결재자 |
| 17 | 상세 | USER01 | 검토자 | `TE` | 보완요청 | `SL` | 기초 | 보완 팝업() → `reqsupplement` |
| 18 | 상세 | USER01 | 승인자 | `TE` | 결재(승인) | `TE` | 다음 결재자 | `process` |
| 19 | 상세 | USER01 | 최종승인자 | `TE` | 결재(회신) | `TP` | 환경청 | `process` 최종 |
| 20 | 수정 | USER05 | - | `SL` | 파일보완완료 | `ER` | 공단 접수 | `submitsupplement` (직전=`ER`) |
| 21 | 수정 | USER05 | - | `SL` | 보완완료 | `TE` | 공단 검토 | `submitsupplement` (직전=`TE`) |
| 22 | 수정 | USER05 | - | `SL` | 보완완료 | `RK` | 공단 검토 | `submitsupplement` (직전=`RK`) |
| 23 | 상세 | USER03 | - | `TP` | 재원협의 결과 회신 | `RM` | 광역 | `tech-review` |
| 24 | 상세 | USER04 | - | `RM` | 지자체회신 | `RB` | 기초 | `updatePrgrsStts` |
| 25 | 상세 | USER05 | - | `RB` | 실무협의서 관리 | 유지 | — | 웹기안기 Open |
| 26 | 상세 | USER05 | - | `RB` | 조치완료 | `RA` | 환경청 | **`submit-actn-result` `isSubmit:true`** |
| 27 | 상세 | USER04 | - | `RC` | 조치결과서제출 | `RA` | 환경청 | `updatePrgrsStts` |
| 28 | 상세 | USER03 | - | `RA` | 실무협의서 관리 | 유지 | — | 웹기안기 Open. **저장 불가** |
| 28a | 상세 | USER03 | - | `RA` | 조치결과 검토의뢰 | `AV` | 공단 검토 | **`request-actn-review`** |
| 29 | 자동화 | AI | - | `AV` | (금액검증) | `EV` | 공단 접수 | FE 호출 없음. 진행 패널 |
| 29a | 상세 | USER01 | 접수자 | `EV` | 검토자지정 | `RK` | 공단 검토 | `POST /elapr-dmnd` cycl=2. 1차 결재선 시드 |
| 30 | 상세 | USER01 | 검토자 | `RK` | 실무협의서관리 | 유지 | — | 웹기안기 |
| 31 | 상세 | USER01 | 검토자 | `RK` | 금액검증 | `RK` | — | `dcrag-to/cost/verify` |
| 32 | 상세 | USER01 | 검토자 | `RK` | 저장 | `RK` | — | `pwrk save-apply` |
| 33 | 상세 | USER01 | 검토자 | `RK` | 검토완료 | `RK` | 다음 결재자 | `process` |
| 34 | 상세 | USER01 | 승인자 | `RK` | 결재(승인) | `RK` | 다음 결재자 | `process` |
| 35 | 상세 | USER01 | 최종승인자 | `RK` | 결재(회신) | `RR` | 환경청 | `process` 최종 |
| 36 | 상세 | USER03 | - | `RR` | 실무협의서 관리(조회), 검토결과 회신 | `EP` | 광역 | `tech-review` |
| 37 | 작성/상세 | USER04 | - | `EP` | 결과회신, 실무협의서 관리(조회) | `TA` | 기초 | `updatePrgrsStts` |
| 38 | 상세 | USER05 | - | `TA` | 결과회신, 실무협의서 관리(조회) | — | — | **API 없음. 조회** |
`SL` 20~22행: 버튼은 직전 상태에 따라 **하나만** 노출. 파일 루프면 [파일보완완료], 그 외 [보완완료].
---
## 3. `button-config` 권장 구조
액션 키는 현행 `FNRS_CNSLTN_VIEW_BUTTON_ACTION`을 확장한다. 신규·이름 변경:
| 신규/변경 액션 | 라벨 | 비고 |
| --- | --- | --- |
| `fileSplmnt` | 파일보완 | `ER` 접수자. 현행에 없음 |
| `fileVrfc` | 파일검증 | 유지. `ER` 재실행 |
| `dsgntRvwr` | 검토자지정 | `ER→AC`(1차), `EV→RK`(2차) |
| `pwrkCnsltnDocMng` | 실무협의서관리 | 유지. `TE`·`RK`·`RB`·`RA`(조회 전용) |
| `pwrkCnsltnDocCrt` | 실무협의서생성 | 유지. 실패/타임아웃만 |
| `rvwRsltInq` | 검토결과조회 | 신규 |
| `actnCmptn` | 조치완료 | `RB→RA`. `isSubmit:true` |
| `actnSbmsn` | 조치결과서제출 | 광역 `RC→RA` |
| `rqstActnRvw` | 조치결과 검토의뢰 | 환경청 `RA→AV` |
| `costVrfc` | 금액검증 | `RK` |
| `lclRply` | 지자체회신 | 광역 `RM→RB` |
| `fnrsRsltRply` | 재원협의 결과 회신 | 환경청 `TP→RM` |
| `rsltRply` | 결과회신 | `LE→EP`, `RR→EP`, `EP→TA`, `TA` 조회 |
| `fileSplmntCmptn` | 파일보완완료 | `SL←ER` |
| `splmntCmptn` | 보완완료 | `SL←TE\|RK` |
**삭제**: `fnrsCnsltnAncmnt`(고시→`TB`), `sncnAprv`(인가승인). `EP`/`TA`는 `rsltRply`로 통일.
### 3-1. 역할 × 상태 (공단 제외)
```
ROLE_USER05
"" | SV → 저장, 신청완료, 삭제
SL → (직전 ER) 파일보완완료 | (직전 TE|RK) 보완완료
RB → 실무협의서 관리, 조치완료
TA → 결과회신 (조회), 실무협의서 관리(조회)
ROLE_USER04
ST → 환경청제출 // 현행 반려(rjct→SV)는 SoT 38행에 없음. 유지 시 별도 합의
RM → 지자체회신
RC → 조치결과서제출
EP → 결과회신, 실무협의서 관리(조회)
ROLE_USER03
LE → 결과회신(자체종결), 기술검토 의뢰
TP → 재원협의 결과 회신
RA → 실무협의서 관리(조회), 조치결과 검토의뢰
RR → 실무협의서 관리(조회), 검토결과 회신 (`tech-review` → `EP`)
```
### 3-2. 공단 결재선 × 상태
```
ER / ATRZ01 접수자
→ 파일보완, 파일검증, 검토자지정
EV / ATRZ01 접수자
→ 검토자지정 (2차 결재선. 1차 결재선 시드, 서식 선택 없음)
TE / ATRZ02 검토자
→ (초안 성공) 실무협의서관리, 검토결과조회, 저장, 검토완료, 보완요청
→ (초안 실패·타임아웃) 실무협의서생성 (+ 위 항목 중 관리/조회는 초안 생긴 뒤)
TE / ATRZ03 승인자 → 결재(승인)
TE / ATRZ99 최종 → 결재(회신)
RK / ATRZ02 검토자
→ (초안 완료) 실무협의서관리
→ (초안 완료 + 결재 대기 ATRZ_PRGRS_002, cycl2) 보완요청, 검토완료
· 보완요청 UI: TE와 동일 SplmntDmndDialog
RK / ATRZ03 → (초안 완료) 실무협의서관리, (cycl2 pending) 결재(승인)
RK / ATRZ99 → (초안 완료) 실무협의서관리, (cycl2 pending) 결재(회신)
· 결재 UI: ElaprApprovalDialog (elaprCycl=2) 승인/반려 → PUT process
```
`FV`, `AC`, `AV`: 액션 빈 배열 + 진행 패널.
`PENDING_APPROVAL_ACTIONS`: `dmndSplmnt`, `rvwCmptn`, `atrzAprv`, `atrzRply`만.
`pwrkCnsltnDocMng`는 결재 대기와 무관(초안 완료만). ATRZ02·ATRZ03·ATRZ99라도 pending 없으면 관리만 노출.
---
## 4. 초안 성공 vs 실패 버튼
`TE`에서 `crtDmndList[].crtSttsComCd` / 폴링 `status`로 분기.
| 조건 | 노출 |
| --- | --- |
| `C` / `COMPLETED` | 실무협의서관리, 검토결과조회, 저장, 검토완료, 보완요청 |
| `F` / `FAILED` 또는 30분 Fallback 후 초안 없음 | 실무협의서생성 |
| `R`/`I` 생성 중 | 진행 패널. 위 버튼 비활성 (`AC`와 동일 UX, 상태가 이미 `TE`면 패널+생성 버튼) |
`RK`는 금액검증 성공이 기본. Fallback 후 [금액검증] 재실행만 추가 노출.
---
## 5. 현행 `button-config.ts`와의 차이
| 항목 | 현재 | To-Be |
| --- | --- | --- |
| USER05 | `""/SV/TA(고시)` | `SL`, `RB` 추가. `TA`는 조회 |
| USER04 | `ST`(제출+반려), `EP`(인가승인) | `RM/RC/EP` 추가. 인가승인 삭제 |
| USER03 | `LE`, `TP`(→EP로 회신) | `RA`, `RR` 추가. `TP`는 `RM` |
| USER01 | `ER`(파일검증, 검토자지정), `TE` | `ER`에 파일보완. `RK` 전면 추가 |
| USER01 ATRZ | `ER`, `TE`만 `needsRoleUser01AtrzResolve` | `RK` 포함 |
File diff suppressed because it is too large Load Diff
-800
View File
@@ -1,800 +0,0 @@
# 실무협의문서 개발 가이드
> **대상 앱**: `frontend/apps/as`
> **작성일**: 2026-08-27
> **버전**: 1.0
> **SoT (백엔드)**:
>
> - `PwrkCnsltnDocCrtRsltController` — `/api/v1/pwrk-cnsltn-doc-crt-rslt`
> - `PwrkCnsltnMngController` — `/api/v1/pwrk-cnsltn-mng`
> - `DcRagApiToController` — `/api/v1/dcrag-to`
> - 초안생성 데이터 설명 (`02.03.초안생성_데이터_설명.md`)
>
> **현재 프론트 상태**: `pwrk-cnsltn-`\* 엔티티·화면 **미구현**. 재원협의(`fnrs-cnsltn`)만 존재한다.
---
## 1. 문서 목적
재원협의 접수자가 **자료 검증 → 실무협의서 초안 생성 → 한글 웹기안기 열람/다운로드 → 편집본 버전 저장**까지 구현할 때 사용하는 프론트엔드 개발 가이드이다.
백엔드 컨트롤러·DTO를 **API source of truth**로 두고, 초안생성 데이터 설명의 업무 시퀀스를 화면 동작에 매핑한다.
---
## 2. 도메인 개요
실무협의서(PWRK)는 재원협의서(FNRS)의 **하위 문서**이다. 문서번호 규칙:
| 문서 | 번호 예시 | 필드 |
| -------- | ----------------- | ----------------------------- |
| 재원협의 | `F-20260824-0001` | `fnrsCnsltnDocNo` / `upDocNo` |
| 실무협의 | `P-20260827-0001` | `pwrkCnsltnDocNo` |
재원협의 1건당 실무협의서는 **1건만** 생성된다. `POST /pwrk-cnsltn-mng`는 `upDocNo`로 기존 문서를 재사용하고, 없으면 문서번호를 발급한다.
### 2-1. 핵심 식별자
| 필드 | 의미 | 비고 |
| ----------------- | ------------------------------ | --------------------------------------------------------- |
| `pwrkCnsltnDocNo` | 실무협의문서번호 | 문서 PK. `P-YYYYMMDD-NNNN` |
| `upDocNo` | 상위문서번호(재원협의) | `upDocSeComCd = "FNRS"` |
| `crtDmndUuid` | 생성요청 UUID | 초안 생성 작업·결과·다운로드의 키 |
| `verSn` | 버전일련번호 | 복합 PK `(crtDmndUuid, verSn)`. 저장 시 서버가 `+1` |
| `orgnlOtptDocCn` | 원본출력문서내용(HWP 바이너리) | **목록/상세 응답에서는 항상** `null`. 다운로드 API만 제공 |
### 2-2. IIPBS 약어
| 한글 | 약어 | 필드 예 |
| -------- | ------ | ----------------------------- |
| 실무 | PWRK | `pwrkCnsltnDocNo` |
| 협의 | CNSLTN | `pwrk-cnsltn-mng` |
| 관리 | MNG | `PwrkCnsltnMng` |
| 문서 | DOC | `pwrkCnsltnDocNo` |
| 생성 | CRT | `crtDmndUuid`, `crtSttsComCd` |
| 요청 | DMND | `crtDmndUuid` |
| 결과 | RSLT | `crtRslt` |
| 버전 | VER | `verSn` |
| 일련번호 | SN | `verSn` |
| 원본 | ORGNL | `orgnlOtptDocCn` |
| 출력 | OTPT | `orgnlOtptDocCn` |
| 내용 | CN | `orgnlOtptDocCn` |
| 완료 | CMPTN | `crtCmptnDt` |
| 일시 | DT | `crtCmptnDt` |
| 검증 | VRFC | `vrfcSttsComCd` |
| 전송 | TRSM | `trsmFileList` |
| 보완 | SPLMNT | `splmntDmndList` |
| 요청자 | RQSTR | `rqstrUuid`, `rqstrNm` |
---
## 3. 전체 업무 흐름
재원협의 **접수자**가 실무협의서 초안을 만드는 경로이다. 프론트는 DC RAG를 직접 호출하지 않고 **as-service**만 호출한다.
```mermaid
sequenceDiagram
autonumber
participant User as 재원협의 접수자
participant FE as AS Frontend
participant AS as as-service
participant RAG as DC RAG
User->>FE: [자료 검증 요청]
FE->>AS: POST /dcrag-to/documents/transfer<br/>{ fnrsCnsltnDocNo }
AS->>RAG: 자료전송 (IF-01)
AS-->>FE: { resultCode: SUCCESS, requestId }
Note over FE: 재원협의 상세 재조회
AS-->>FE: esntlFileList / etcFileList 에<br/>vrfcDt, vrfcRsltCn, vrfcSttsComCd
User->>FE: [초안 생성]
FE->>AS: POST /pwrk-cnsltn-mng<br/>{ upDocNo, upDocSeComCd: FNRS, prgrsSttsComCd: DV }
alt 실무협의서 번호 없음
AS->>AS: P-번호 발급 + 문서 등록
else 이미 존재
AS->>AS: 기존 pwrkCnsltnDocNo 재사용
end
AS->>RAG: 초안생성 비동기 요청 (IF-03-1)
AS-->>FE: 201 { pwrkCnsltnDocNo, crtDmndUuid, crtSttsComCd: R, estimatedMinutes: 10 }
loop 약 10분, status !== COMPLETED/FAILED
FE->>AS: GET /dcrag-to/documents/draft/status/{crtDmndUuid}
AS-->>FE: { status, events[], progressPercent }
end
Note over FE: events 에 COMPLETED 가 있으면<br/>웹기안기 열기 / 다운로드 가능
User->>FE: [문서 열기] 또는 [다운로드]
FE->>AS: GET /pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/webhwp/open<br/>또는 /download
User->>FE: 웹기안기 편집 후 [저장]
FE->>AS: POST /pwrk-cnsltn-doc-crt-rslt (multipart)<br/>crtDmndUuid + file
AS-->>FE: 201 { verSn: N+1, ... }
```
### 3-1. 프론트가 호출하면 안 되는 API
| API | 이유 |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `POST /dcrag-to/documents/draft/generate` | 초안 생성은 `POST /pwrk-cnsltn-mng`가 문서번호 발급 + DC RAG 호출을 **한 트랜잭션**으로 처리한다. 프론트는 관리 API만 호출한다. |
---
## 4. 화면 데이터 흐름
초안 생성은 **재원협의 상세**에서 시작하고, 실무협의 목록/상세는 별도 화면이다.
```
BEGIN
// ===== 재원협의 상세 (기존 fnrs-cnsltn view) =====
CALL findById(fnrsCnsltnDocNo)
DISPLAY 재원협의 상세 + 필수/기타 첨부
ON [자료 검증 요청] 버튼 CLICK DO
CALL requestDocumentTransfer({ fnrsCnsltnDocNo })
CALL findById(fnrsCnsltnDocNo) // 검증결과 필드 반영
DISPLAY 첨부 리스트 (vrfcSttsComCd: PASS | FAIL)
END ON
ON [초안 생성] 버튼 CLICK DO
CALL pwrkCnsltnMngService.save({
upDocNo: fnrsCnsltnDocNo,
upDocSeComCd: "FNRS",
prgrsSttsComCd: "DV"
})
// → pwrkCnsltnDocNo, crtDmndUuid, estimatedMinutes
START POLL getDraftGenerationStatus(crtDmndUuid)
DISPLAY 초안 생성 진행률 (약 10분)
END ON
ON POLL status = COMPLETED 또는 events.eventType 에 COMPLETED DO
ENABLE [웹기안기 열기] [다운로드]
END ON
ON [웹기안기 열기] 버튼 CLICK DO
CALL HwpCtrl.Open( webhwpOpenUrl(crtDmndUuid) ) // TBD: 컨트롤러 미구현
END ON
ON [다운로드] 버튼 CLICK DO
CALL downloadLatest(crtDmndUuid) // blob + Content-Disposition
SAVE 로컬 파일
END ON
// ===== 실무협의 목록 =====
CALL pwrkCnsltnMngService.findPage(searchDto + pageable)
DISPLAY 실무협의 문서 그리드
ON 행 CLICK DO
NAVIGATE 실무협의 상세 (pwrkCnsltnDocNo)
END ON
// ===== 실무협의 상세 / 재원협의에서 연계 조회 =====
CALL findById(pwrkCnsltnDocNo)
// 또는 재원협의 번호로: findByUpDocNo(fnrsCnsltnDocNo)
DISPLAY 마스터 + cnsltnList + pjtcoAjmtList + crtDmndList.crtRslt
// crtRslt.orgnlOtptDocCn 은 항상 null — 바이너리는 download API
ON 버전 이력 조회 DO
CALL pwrkCnsltnDocCrtRsltService.findPage({ crtDmndUuid, sort: "verSn,desc" })
DISPLAY 버전 목록
END ON
ON 웹기안기 편집 완료 [저장] DO
CALL saveMultipart({ crtDmndUuid, file }) // 서버가 verSn + 1
INVALIDATE 상세·버전 목록
END ON
END
```
---
## 5. API 명세
### 5-1. 공통
| 항목 | 값 |
| ----------- | ----------------------------------------------------------------- |
| Gateway | `/as-service` |
| Base prefix | `ENDPOINT.AS_SERVICE.*` (`apps/as/src/shared/config/api.ts`) |
| Auth 헤더 | `Authorization`, `X-Site-Id`, `token-id` (기존 `client` 인터셉터) |
| 날짜 포맷 | `yyyy-MM-dd HH:mm:ss` |
추가할 ENDPOINT:
```ts
AS_SERVICE: {
// 기존 …
PWRK_CNSLTN_MNG: "/as-service/api/v1/pwrk-cnsltn-mng",
PWRK_CNSLTN_DOC_CRT_RSLT: "/as-service/api/v1/pwrk-cnsltn-doc-crt-rslt",
DCRAG_TO: "/as-service/api/v1/dcrag-to",
}
```
---
### 5-2. DC RAG 연계 (`DcRagApiToController`)
프론트 엔티티 슬라이스: `entities/dcrag-to/`
| 기능 | Controller | Method | URL | Request | Response |
| ----------------- | -------------------------- | ------ | ------------------------------------------------ | --------------------------------- | ------------------------------------- |
| 자료전송요청 | `requestDocumentTransfer` | POST | `/dcrag-to/documents/transfer` | `DcRagDocumentTransferRequestDto` | `DcRagCommonResponseDto` |
| 초안생성 진행상태 | `getDraftGenerationStatus` | GET | `/dcrag-to/documents/draft/status/{crtDmndUuid}` | path | `DcRagDraftStatusResponseDto` |
| 금액검증요청 | `requestCostVerification` | POST | `/dcrag-to/documents/cost/verify` | `DcRagCostVerifyRequestDto` | `DcRagCommonResponseDto` (202) |
| 초안생성요청 | `requestDraftGeneration` | POST | `/dcrag-to/documents/draft/generate` | `DcRagDraftGenerateRequestDto` | `DcRagDraftGenerateResponseDto` (202) |
**화면에서 쓰는 것**: 자료전송 + 상태 폴링 (+ 추후 금액검증).
초안생성요청은 5-3의 `pwrkCnsltnMngService.save`로 대체한다.
#### 자료전송 Request / Response
```json
POST /as-service/api/v1/dcrag-to/documents/transfer
{ "fnrsCnsltnDocNo": "F-20260824-0001" }
```
```json
{
"resultCode": "SUCCESS",
"message": "자료전송요청 접수 완료",
"requestId": "F-20260824-0001"
}
```
자료 검증이 **완료된 뒤** 재원협의 상세를 다시 조회하면, 필수첨부(`esntlFileList`)·기타첨부(`etcFileList`) 각 파일에 검증 결과가 붙는다.
| 필드 | 예 | 의미 |
| --------------- | ----------------------------- | ------------ |
| `vrfcDt` | `2026-08-25 18:13:49` | 검증일시 |
| `vrfcRsltCn` | `정상 확인 (재원협의 신청서)` | 검증결과내용 |
| `vrfcSttsComCd` | `PASS` / `FAIL` | 검증상태 |
프론트 `FnrsCnsltnMngDetResponseDto` / `AtflMngClientResponseDto` / `EsntlAtchFileMngTreeResponseDto`에 위 3필드가 **아직 없다**. 재원협의 상세 첨부 영역에 배지를 그리려면 타입을 확장해야 한다.
#### 초안생성 상태 폴링
```http
GET /as-service/api/v1/dcrag-to/documents/draft/status/{crtDmndUuid}
```
| 필드 | 설명 |
| ----------------------- | --------------------------------------------------------------------------------- |
| `status` | `REQUESTED` / `DOWNLOADING` / `ANALYZING` / `GENERATING` / `COMPLETED` / `FAILED` |
| `statusName` | 표시명 (예: `생성 완료`) |
| `progressPercent` | 0~100 |
| `events[]` | `{ eventType, eventMessage, occurredDt }` |
| `startedDt` | 시작일시 |
| `estimatedCompletionDt` | 예상 완료일시 |
| `errorMessage` | 실패 시 메시지 |
폴링 규칙:
- 주기: **5~10초** (TBD, UX 확정).
- 예상 소요: **약 10분** (`estimatedMinutes`).
- 종료 조건: `status === "COMPLETED"` **또는** `events`에 `eventType === "COMPLETED"`.
- 실패: `status === "FAILED"` → 폴링 중단, `errorMessage` 표시.
- 완료 전에는 웹기안기 열기·다운로드 버튼을 **비활성**.
이벤트 순서 예시:
| eventType | eventMessage |
| ------------- | --------------------------- |
| `REQUESTED` | 요청을 접수했습니다. |
| `DOWNLOADING` | 신청서를 수신하는 중입니다. |
| `ANALYZING` | 자료 준비가 완료되었습니다. |
| `GENERATING` | 문서 저장이 완료되었습니다. |
| `COMPLETED` | 문서 생성이 완료되었습니다. |
---
### 5-3. 실무협의관리 (`PwrkCnsltnMngController`)
프론트 엔티티 슬라이스: `entities/pwrk-cnsltn-mng/`
| 기능 | Controller | Method | URL | Request | Response |
| ------------------- | ---------------------- | ------ | ----------------------------------------------------- | ----------------------------------- | ------------------------------------ |
| 페이징 조회 | `findPage` | GET | `/pwrk-cnsltn-mng` | `PwrkCnsltnMngSearchDto` + Pageable | `Page<PwrkCnsltnMngResponseDto>` |
| 상세 (문서번호) | `findById` | GET | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}` | path | `PwrkCnsltnMngDetResponseDto` |
| 상세 (재원협의번호) | `findByUpDocNo` | GET | `/pwrk-cnsltn-mng/updocno/{upDocNo}` | path | `PwrkCnsltnMngDetResponseDto` |
| 등록 + 초안생성 | `save` | POST | `/pwrk-cnsltn-mng` | `PwrkCnsltnMngSaveRequestDto` | `PwrkCnsltnMngSaveResponseDto` (201) |
| 신청 통합 저장 | `saveApply` | POST | `/pwrk-cnsltn-mng/save-apply` | `PwrkCnsltnApplySaveRequestDto` | `string` (문서번호, 201) |
| 수정 | `update` | PUT | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}` | `PwrkCnsltnMngUpdateRequestDto` | `string` |
| 삭제 | `delete` → `remove` | DELETE | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}` | path | 204 |
| 보완완료 제출 | `submitSupplement` | POST | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}/submitsupplement` | `PwrkCnsltnApplySaveRequestDto` | `string` |
| 보완요청 등록 | `submitReqSupplement` | POST | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}/reqsupplement` | `PwrkCnsltnApplySaveRequestDto` | `string` |
| 진행상태 수정 | `changePrgrsSttsComCd` | POST | `/pwrk-cnsltn-mng/doc/updatePrgrsStts` | `DocPrgrsUpdateRequestDto` | 200 |
목록 기본 호출:
```http
GET /as-service/api/v1/pwrk-cnsltn-mng?page=0&size=10&sort=pwrkCnsltnDocNo,desc
```
#### 초안 생성 Request / Response (`save`)
```json
POST /as-service/api/v1/pwrk-cnsltn-mng
{
"upDocNo": "F-20260824-0001",
"upDocSeComCd": "FNRS",
"prgrsSttsComCd": "DV"
}
```
```json
{
"pwrkCnsltnDocNo": "P-20260826-0001",
"upDocNo": "F-20260824-0001",
"upDocSeComCd": "FNRS",
"prgrsSttsComCd": "DV",
"resultCode": "ACCEPTED",
"message": "초안 생성 작업이 접수되었습니다.",
"crtDmndUuid": "382f2821-9231-42e6-8309-4a1718a2ead8",
"crtSttsComCd": "R",
"requestId": "382f2821-9231-42e6-8309-4a1718a2ead8",
"estimatedMinutes": 10
}
```
백엔드 동작 (`PwrkCnsltnMngService.save`):
1. `upDocNo`로 기존 실무협의서가 있으면 **그 번호를 재사용** (재원협의 1 : 실무협의 1).
2. 없으면 `docNoMngService.issueDocNo(PWRK)`로 `P-` 번호 발급 후 insert.
3. DC RAG 초안생성을 비동기로 호출하고 `crtDmndUuid`를 응답에 실어 반환.
4. DC RAG 실패 시에도 **문서는 롤백하지 않고** 로그만 남긴다. 프론트는 `resultCode`가 `ACCEPTED`/`SUCCESS`가 아니면 토스트로 실패를 알린다.
#### 상세 응답 구조 (`PwrkCnsltnMngDetResponseDto`)
재원협의 상세에서 `GET .../updocno/{fnrsCnsltnDocNo}`로 연계 데이터를 한 번에 가져온다.
```json
{
"pwrkCnsltnDocNo": "P-20260827-0001",
"upDocNo": "F-20260824-0001",
"upDocSeComCd": "FNRS",
"prgrsSttsComCd": "DV",
"cnsltnList": [],
"pjtcoAjmtList": [],
"crtDmndList": [
{
"crtDmndUuid": "d0545bef-735f-4d7b-a781-88e96e03577a",
"pwrkCnsltnDocNo": "P-20260827-0001",
"rqstrUuid": "65a00f65-8460-49af-98ec-042977e56f4b",
"rqstrNm": "관리자",
"crtSttsComCd": "C",
"crtRslt": {
"crtDmndUuid": "d0545bef-735f-4d7b-a781-88e96e03577a",
"verSn": 1,
"rgtrUuid": "...",
"rgtrNm": "관리자",
"orgnlOtptDocCn": null,
"crtCmptnDt": "2026-08-27 10:47:01"
}
}
],
"splmntDmndList": [],
"trsmFileList": []
}
```
`crtRslt`는 최신 버전 메타만 포함한다. `orgnlOtptDocCn`은 절대 쓰지 말고 download API를 호출한다.
마스터에 재원협의 사업정보가 조인된다: `bizNm`, `admdstCd`, `envoSeComCd`, `taskSeComCd`, `aplcntNm` 등.
---
### 5-4. 문서생성결과 (`PwrkCnsltnDocCrtRsltController`)
프론트 엔티티 슬라이스: `entities/pwrk-cnsltn-doc-crt-rslt/`
복합 PK: `crtDmndUuid` + `verSn`
| 기능 | Controller | Method | URL | Request | Response |
| ------------------ | ------------------------- | ------ | ---------------------------------- | ----------------------------------------------- | --------------------------------------- |
| 버전 이력 페이징 | `findPage` | GET | `/pwrk-cnsltn-doc-crt-rslt` | `PwrkCnsltnDocCrtRsltSearchDto` + Pageable | `Page<PwrkCnsltnDocCrtRsltResponseDto>` |
| 특정 버전 상세 | `findById` | GET | `/{crtDmndUuid}/{verSn}` | path | `PwrkCnsltnDocCrtRsltResponseDto` |
| 최신 버전 상세 | `findLatestByCrtDmndUuid` | GET | `/{crtDmndUuid}/latest` | path | `PwrkCnsltnDocCrtRsltResponseDto` |
| 특정 버전 다운로드 | `download` | GET | `/{crtDmndUuid}/{verSn}/download` | path | `byte[]` (attachment) |
| 최신 버전 다운로드 | `downloadLatest` | GET | `/{crtDmndUuid}/download` | path | `byte[]` (attachment) |
| 등록 (multipart) | `saveMultipart` | POST | `/` `multipart/form-data` | `PwrkCnsltnDocCrtRsltSaveRequestDto` + `file` | `PwrkCnsltnDocCrtRsltResponseDto` (201) |
| 등록 (JSON) | `saveJson` | POST | `/` `application/json` | `PwrkCnsltnDocCrtRsltSaveRequestDto` | 201 |
| 수정 (multipart) | `updateMultipart` | PUT | `/{crtDmndUuid}/{verSn}` multipart | `PwrkCnsltnDocCrtRsltUpdateRequestDto` + `file` | ResponseDto |
| 수정 (JSON) | `updateJson` | PUT | `/{crtDmndUuid}/{verSn}` json | UpdateRequestDto | ResponseDto |
| 삭제 | `delete` → `remove` | DELETE | `/{crtDmndUuid}/{verSn}` | path | 204 |
Spring 매핑 주의: `/{crtDmndUuid}/latest` 와 `/{crtDmndUuid}/download` 는 `/{crtDmndUuid}/{verSn}` 보다 **구체 경로가 우선**한다. 프론트 path를 섞지 말 것.
버전 이력 조회 예:
```http
GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt?page=0&size=10&crtDmndUuid={uuid}&sort=verSn,desc
```
#### 다운로드 (attachment)
화면 [다운로드] 버튼 → PC 저장.
```http
GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/download
GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/{verSn}/download
```
- axios: `responseType: "blob"`.
- 파일명: `Content-Disposition`의 `filename*=UTF-8''...` (`parseFilenameFromResponse` 재사용).
- 서버 파일명 규칙: `재원협의 실무협의서({bizNm})_v{verSn}.hwp`.
- **현재 컨트롤러 Content-Type**: `application/x-hwp`.
데이터 설명 예시는 `application/haansofthwpx`. 프론트는 Content-Type에 의존하지 말고 blob으로 저장한다.
기존 `attachService` 다운로드와 동일 패턴:
```ts
const response = await client.get<Blob>(url, { responseType: "blob" });
const filename = parseFilenameFromResponse(response);
const objectUrl = window.URL.createObjectURL(response.data);
```
#### 웹기안기 열기 (inline) — TBD
데이터 설명 기준 엔드포인트. **현재** `PwrkCnsltnDocCrtRsltController`**에는 없음.** 백엔드 추가 후 연결한다.
```http
GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/webhwp/open
GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/{verSn}/webhwp/open
```
의도:
- `Content-Disposition: inline`
- 한컴 웹기안기 `HwpCtrl.Open(url)` 이 이 URL을 직접 호출
- 브라우저 다운로드 창이 뜨지 않고 캔버스에 로드
프론트는 **blob fetch 후 Object URL을 넘기지 않는다.** 인증 헤더가 필요한 절대 URL을 기안기에 전달하는 방식이므로, 쿠키/게이트웨이 인증 가능 여부를 백엔드와 확인한다 (TBD).
#### 신규 버전 등록 (웹기안기 저장)
서버가 최신 `verSn + 1`을 부여한다. 클라이언트가 `verSn`을 보내지 않는다.
```http
POST /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt
Content-Type: multipart/form-data
crtDmndUuid: {uuid}
file: 재원협의 실무협의서_v2.hwpx
```
화면에서는 **multipart만** 사용한다. JSON `saveJson`은 바이너리를 body에 넣는 경로라 UI에서 쓰지 않는다.
`file` part 이름: `"file"`. DTO 필드는 `@ModelAttribute` 로 평문 필드(`crtDmndUuid`) 또는 `saveDto` part.
---
## 6. DTO 매핑 (프론트 타입명 = 백엔드 클래스명)
### 6-1. `PwrkCnsltnDocCrtRslt*`
| 계층 | 파일 | 정의 |
| ------------------------------------ | -------------------------------------------- | ----------------------------------------------- |
| Save Request | `pwrk-cnsltn-doc-crt-rslt-payload-schema.ts` | zod SSOT → `PwrkCnsltnDocCrtRsltSaveRequestDto` |
| Response / Search / Update / Service | `pwrk-cnsltn-doc-crt-rslt-interface.ts` | `interface` |
**Response**
| 필드 | 타입 | 비고 |
| ---------------- | -------- | --------------------- |
| `crtDmndUuid` | `string` | UUID |
| `verSn` | `number` | BigDecimal |
| `rgtrUuid` | `string` | |
| `rgtrNm` | `string` | 서버 enrich |
| `orgnlOtptDocCn` | `null` | 목록/상세 미포함 |
| `crtCmptnDt` | `string` | `yyyy-MM-dd HH:mm:ss` |
**Search** (`PageableParams` 확장)
| 필드 | 비고 |
| ----------------- | ---------------------- |
| `crtDmndUuid` | 버전 이력 조회 시 필수 |
| `pwrkCnsltnDocNo` | |
| `verSn` | |
| `rgtrUuid` | |
**Save Request (zod)**
| 필드 | 필수 | 비고 |
| ------------- | ---- | ------------------------------------ |
| `crtDmndUuid` | Y | |
| `rgtrUuid` | N | 미전송 시 서버가 로그인 사용자 사용 |
| `file` | UI | multipart `file` part. DTO 필드 아님 |
**Update Request**
| 필드 | 비고 |
| ------------ | --------- |
| `rgtrUuid` | |
| `crtCmptnDt` | |
| `file` | multipart |
### 6-2. `PwrkCnsltnMng*`
**Search**
`pwrkCnsltnDocNo`, `upDocNo`, `upDocSeComCd`, `prgrsSttsComCd`, `admdstCd`, `envoSeComCd`, `taskSeComCd`, `bizNm`
**Save Request (초안 생성)** — 필드가 얇으므로 interface만으로도 가능. 권장은 zod.
| 필드 | 필수 | 값 |
| ----------------- | ---- | ----------------- |
| `upDocNo` | Y | 재원협의 문서번호 |
| `upDocSeComCd` | | `"FNRS"` |
| `prgrsSttsComCd` | | `"DV"` (초안생성) |
| `pwrkCnsltnDocNo` | N | 서버 발급 |
**Save Response** — `save`만 이 DTO. `fnrsCnsltnMngService.save`가 `string`을 반환하는 것과 **다름**.
| 필드 | 의미 |
| ------------------ | --------------------------------- |
| `pwrkCnsltnDocNo` | 발급/재사용 번호 |
| `resultCode` | `ACCEPTED` / `SUCCESS` / 실패코드 |
| `crtDmndUuid` | 폴링 키 |
| `crtSttsComCd` | `R` 생성요청 |
| `estimatedMinutes` | 기본 10 |
**DetResponse 중첩**
- `cnsltnList`: 협의내역 (`rvwOpnnSn`, `rvwOpnnClsfComCd`, `cnsltnCn`, `rfltYn`, …)
- `pjtcoAjmtList`: 사업비조정
- `crtDmndList[].crtRslt`: 생성요청 + 최신 결과
- `splmntDmndList`: 보완요청
- `sttsChgHstryList`: 상태변경이력
- `trsmFileList`: 전송파일 (데이터 설명 기준. Det DTO Java 필드 확인 후 타입 반영)
### 6-3. DC RAG
```ts
interface DcRagDocumentTransferRequestDto {
fnrsCnsltnDocNo: string;
taskSeComCd?: string;
taskSeCdNm?: string;
}
interface DcRagCommonResponseDto {
resultCode: string;
message?: string;
requestId?: string;
}
interface DcRagDraftStatusResponseDto {
crtDmndUuid: string;
status: DraftStatus;
statusName?: string;
progressPercent?: number;
events?: EventInfoDto[];
startedDt?: string;
estimatedCompletionDt?: string;
errorMessage?: string | null;
}
type DraftStatus =
| "REQUESTED"
| "DOWNLOADING"
| "ANALYZING"
| "GENERATING"
| "COMPLETED"
| "FAILED";
```
---
## 7. 공통코드
| 상위코드 | 필드 | 값 | 명칭 |
| ----------------------- | ---------------- | --------------- | ------------- |
| `ASDOCSE` | `upDocSeComCd` | `FNRS` | 재원협의 |
| `ASDOCSE` | 문서구분 | `PWRK` | 실무협의 |
| `PWRKSTTS` | `prgrsSttsComCd` | `DV` | 초안생성 |
| `PWRKSTTS` | | `AR` | 검토자지정 |
| `PWRKSTTS` | | `SV` | 작성중 |
| `PWRKSTTS` | | `RB` | 검토회신 |
| `PWRKSTTS` | | `RC` | 협의회신 |
| `PWRKSTTS` | | `TR` | 검토결과 회신 |
| `PWRKSTTS` | | `SL` | 보완요청 |
| `PWRKSTTS` | | `AA` | 금액조정중 |
| `AICRTSTTS` / `CRTSTTS` | `crtSttsComCd` | `R` | 생성요청 |
| | | `I` | 생성중 |
| | | `C` | 생성완료 |
| | | `F` | 생성실패 |
| (검증) | `vrfcSttsComCd` | `PASS` / `FAIL` | |
`CrtDmndResponseDto`는 `@CodeName(upCd = "CRTSTTS")`, 생성요청 단건 DTO는 `AICRTSTTS`를 쓴다. 코드값이 `R/I/C/F`로 같으므로 UI는 코드값으로 분기하고, 표시명은 응답의 `*Nm` 필드를 쓴다.
초안 생성 직후 `crtSttsComCd = R`, 폴링 `COMPLETED` 이후 상세의 `crtSttsComCd = C`.
---
## 8. 프론트엔드 구현 가이드
### 8-1. 기술 스택·원칙
- App Router + FSD (`app/` → `pages/` → `widgets/` → `entities/` → `shared/`)
- React Query. ad-hoc `useQuery` + service 직접 호출 금지
- `@common/components`, `useDataTable`, `@tanstack/react-table`
- `'use client'`는 Web API(다운로드, 폴링, 웹기안기)가 있는 페이지만
- 함수명 = Controller public 메서드명 (`delete` → `remove`)
- 타입명 = 백엔드 DTO명. 프론트 전용 축약 금지
- Save/Apply Request = zod SSOT. Response는 `interface`
참조 슬라이스: `entities/fnrs-cnsltn/`, `entities/sewer-bsc-plan/`
### 8-2. 모듈·파일 배치
| 구분 | 경로 |
| -------------------- | ------------------------------------------------------------------- |
| ENDPOINT | `apps/as/src/shared/config/api.ts` |
| 실무협의 관리 엔티티 | `entities/pwrk-cnsltn-mng/` |
| 생성결과 엔티티 | `entities/pwrk-cnsltn-doc-crt-rslt/` |
| DC RAG 엔티티 | `entities/dcrag-to/` |
| 목록 라우트 | `app/(main)/(with-base-layout)/pwrk-cnsltn/page.tsx` |
| 상세 라우트 | `app/(main)/(with-base-layout)/pwrk-cnsltn/[id]/page.tsx` |
| 페이지 UI | `pages/pwrk-cnsltn/` |
| 재원협의 상세 연계 | `pages/fnrs-cnsltn/ui/fnrs-view-page.tsx` (버튼·폴링·첨부 검증배지) |
엔티티 내부:
```
entities/pwrk-cnsltn-mng/
├── model/
│ ├── pwrk-cnsltn-mng-interface.ts
│ └── pwrk-cnsltn-mng-payload-schema.ts
├── api/
│ ├── pwrk-cnsltn-mng-service.ts
│ └── queries.ts
└── index.ts
entities/pwrk-cnsltn-doc-crt-rslt/
├── model/
│ ├── pwrk-cnsltn-doc-crt-rslt-interface.ts
│ └── pwrk-cnsltn-doc-crt-rslt-payload-schema.ts
├── api/
│ ├── pwrk-cnsltn-doc-crt-rslt-service.ts
│ └── queries.ts
└── index.ts
```
### 8-3. Service 시그니처 요지
`pwrkCnsltnMngService.save` 반환 타입이 `string`이 아니라 `PwrkCnsltnMngSaveResponseDto` 인 점을 `fnrs-cnsltn`과 혼동하지 말 것.
```ts
export interface PwrkCnsltnMngService {
findPage(searchDto: PwrkCnsltnMngSearchDto): Promise<PageResponse<PwrkCnsltnMngResponseDto>>;
findById(pwrkCnsltnDocNo: string): Promise<PwrkCnsltnMngDetResponseDto>;
findByUpDocNo(upDocNo: string): Promise<PwrkCnsltnMngDetResponseDto>;
save(body: PwrkCnsltnMngSaveRequestDto): Promise<PwrkCnsltnMngSaveResponseDto>;
saveApply(body: PwrkCnsltnApplySaveRequestDto): Promise<string>;
update(pwrkCnsltnDocNo: string, body: PwrkCnsltnMngUpdateRequestDto): Promise<string>;
submitSupplement(pwrkCnsltnDocNo: string, body: PwrkCnsltnApplySaveRequestDto): Promise<string>;
submitReqSupplement(
pwrkCnsltnDocNo: string,
body: PwrkCnsltnApplySaveRequestDto,
): Promise<string>;
remove(pwrkCnsltnDocNo: string): Promise<void>;
changePrgrsSttsComCd(body: DocPrgrsUpdateRequestDto): Promise<void>;
}
export interface PwrkCnsltnDocCrtRsltService {
findPage(
searchDto: PwrkCnsltnDocCrtRsltSearchDto,
): Promise<PageResponse<PwrkCnsltnDocCrtRsltResponseDto>>;
findById(crtDmndUuid: string, verSn: number): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
findLatestByCrtDmndUuid(crtDmndUuid: string): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
download(crtDmndUuid: string, verSn: number): Promise<AxiosResponse<Blob>>;
downloadLatest(crtDmndUuid: string): Promise<AxiosResponse<Blob>>;
saveMultipart(formData: FormData): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
updateMultipart(
crtDmndUuid: string,
verSn: number,
formData: FormData,
): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
remove(crtDmndUuid: string, verSn: number): Promise<void>;
}
```
multipart `Content-Type`은 axios가 boundary를 붙이도록 **헤더를 수동 지정하지 않는다.**
### 8-4. 폴링 훅
`useQuery` + `refetchInterval`:
```ts
useQuery({
queryKey: [...dcragToKeys.draftStatus(crtDmndUuid)],
queryFn: () => dcragToService.getDraftGenerationStatus(crtDmndUuid),
enabled: Boolean(crtDmndUuid) && isPolling,
refetchInterval: (query) => {
const status = query.state.data?.status;
if (status === "COMPLETED" || status === "FAILED") return false;
return 5_000;
},
});
```
`COMPLETED` 시 `pwrkCnsltnMng` 상세 쿼리를 invalidate 하여 `crtDmndList[].crtRslt`를 갱신한다.
### 8-5. 재원협의 상세에 붙일 UI
`pages/fnrs-cnsltn` 조회 모드:
1. **자료 검증 요청** — `requestDocumentTransfer` → 상세 refetch → 첨부 행에 PASS/FAIL 배지 (`vrfcSttsComCd`, `vrfcRsltCn`, `vrfcDt`).
2. **초안 생성** — 검증이 끝난 뒤 활성. `pwrkCnsltnMngService.save`.
3. **진행 표시** — `progressPercent` + 이벤트 타임라인. `estimatedMinutes`(10)를 안내.
4. **웹기안기 열기 / 다운로드** — `COMPLETED` 이후에만.
`findByUpDocNo(fnrsCnsltnDocNo)`로 이미 초안이 있으면 `crtDmndUuid`를 복원해 폴링·열기를 이어간다.
### 8-6. 실무협의 목록 컬럼 (권장)
마스터 응답 + 재원협의 조인 필드 기준. 화면설계 확정 전 TBD.
| 표시명 | Field |
| ------------ | ------------------ |
| 실무협의번호 | `pwrkCnsltnDocNo` |
| 재원협의번호 | `upDocNo` |
| 진행상태 | `prgrsSttsComCdNm` |
| 사업명 | `bizNm` |
| 시도/구군 | `sido` / `gugun` |
| 신청자 | `aplcntNm` |
| 등록일 | `regDt` |
행 클릭 → `/pwrk-cnsltn/{pwrkCnsltnDocNo}`.
---
## 9. 구현 순서
```
Task Progress:
- [ ] 1. ENDPOINT 3개 추가 (PWRK_CNSLTN_MNG, PWRK_CNSLTN_DOC_CRT_RSLT, DCRAG_TO)
- [ ] 2. entities/dcrag-to (transfer + draft status)
- [ ] 3. entities/pwrk-cnsltn-mng (findPage, findById, findByUpDocNo, save)
- [ ] 4. entities/pwrk-cnsltn-doc-crt-rslt (findPage, downloadLatest, saveMultipart)
- [ ] 5. 재원협의 상세: 검증결과 3필드 타입 확장 + 배지
- [ ] 6. 재원협의 상세: 자료검증 / 초안생성 / 폴링 UI
- [ ] 7. 실무협의 목록·상세 라우트
- [ ] 8. 다운로드 (blob)
- [ ] 9. 웹기안기 Open — 백엔드 webhwp/open 확인 후
- [ ] 10. 웹기안기 저장 → saveMultipart (verSn + 1)
```
1~4는 엔티티 레이어만으로 먼저 닫는다. 화면은 5부터.
---
## 10. TBD · 백엔드 확인
| 항목 | 내용 |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `webhwp/open` | 데이터 설명에는 있으나 **현재 Controller에 없음**. 추가 일정·인증 방식 확인 |
| Content-Type | Controller `application/x-hwp` vs 설명 `application/haansofthwpx` |
| 파일 확장자 | 다운로드 파일명이 `.hwp`로 고정. 실제 바이너리가 hwpx일 수 있음 |
| 폴링 주기 | 5초 vs 10초, 탭 비활성 시 중단 여부 |
| 검증 필드 타입 | `FnrsCnsltnMngDetResponseDto.esntlFileList` / `etcFileList` Java 타입이 아직 `FnrsCnsltnAtflResponseDto`가 아님. 런타임 JSON에 `vrfc*`가 오는지 확인 |
| `trsmFileList` | 데이터 설명 상세 예시에는 있으나 Det DTO Java 선언과 불일치 가능 |
| `crtSttsComCd` 상위코드 | `CRTSTTS` vs `AICRTSTTS` |
| 웹기안기 라이선스 URL | `HwpCtrl.Open`에 넘길 게이트웨이 절대 경로 (쿠키 인증 가능 여부) |
| 초안 생성 1회 제한 | `requestDraftGeneration` 주석에 “1회 제한”. 재클릭 UX (비활성 / 재요청 확인) |
| 자료전송 허용 확장자 | `.doc .docx .hwp .hwpx .md .pdf .txt .xls .xlsm .xlsx`. 이미지 첨부 시 DC RAG 실패 |
---
## 11. 참고
| 자료 | 위치 |
| ---------------------- | ------------------------------------------------------------------------------ |
| 생성결과 컨트롤러 | `backend/.../pwrkcnsltndoccrtrslt/PwrkCnsltnDocCrtRsltController.java` |
| 실무협의관리 컨트롤러 | `backend/.../pwrkcnsltnmng/PwrkCnsltnMngController.java` |
| DC RAG TO 컨트롤러 | `backend/.../dcrag/DcRagApiToController.java` |
| 초안생성 데이터 설명 | `02.03.초안생성_데이터_설명.md` |
| 엔티티 구현 스킬 | `frontend/.cursor/skills/backend-controller-to-entity/` |
| 재원협의 엔티티 (패턴) | `apps/as/src/entities/fnrs-cnsltn/` |
| IIPBS 단어목록 | `frontend/docs/IIPBS_단어목록.md` |
| 진행상태 코드 | `backend/IIPBS_CS/docs/2026/05.행정지원/01.문서상태별_사용자_상태변경_이력.md` |
@@ -33,10 +33,10 @@
├──────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ [영역 B] 필수첨부파일 목록 │
│ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ 필수첨부파일아이디 | 문서구분 | 필수첨부파일명 | 필수첨부여부 | 비고 | 등록일시 | 수정일시 | 관리 │
│ │ 필수첨부파일아이디 | 문서구분 | 필수첨부파일명 | 필수첨부여부 | 필수파일검토내용 | 비고 | 관리 │ │
│ ├────────────────────────────────────────────────────────────────────────────────────────────────┤ │
│ │ EAF-0001 | 설치인가 | 사업계획서 | 예 | 서명본 첨부 | 2026-04-22 10:10:00 | — | [수정][삭제] │ │
│ │ EAF-0002 | 기본계획 | 변경사유서 | 예 | — | 2026-04-22 11:00:00 | — | [수정][삭제] │ │
│ │ EAF-0001 | 설치인가 | 사업계획서 | 예 | 사업비 내역 검토 | 서명본 첨부 | [수정][삭제] │ │
│ │ EAF-0002 | 기본계획 | 변경사유서 | 예 | 변경 전후 비교표 | — | [수정][삭제] │ │
│ └────────────────────────────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────┘
@@ -47,7 +47,9 @@
│ 필수첨부파일아이디(자동발급) [EAF-0004] │
│ 문서구분 [설치인가 ▼] │
│ 필수첨부파일명 [예: 사업계획서] │
│ 양식파일 [파일 선택] │
│ 필수첨부여부 [ ] 예 [ ] 아니오 │
│ 필수파일검토내용 [검토내용을 입력하세요]│
│ 비고 [비고를 입력하세요] │
├──────────────────────────────────────────────────────┤
│ [취소] [저장] │
@@ -139,16 +141,16 @@ END
### 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 | 미수정 시 빈 값 또는 `—` |
| 관리 | — | — | 행별 **[수정] · [삭제]** |
| 컬럼명 | Field | 타입 | 비고 |
| ------------------ | -------------------- | ------ | ------------------------------------- |
| 필수첨부파일아이디 | `esntlAtchFileId` | string | 예: `EAF-0001`, 클라이언트 생성/입력 |
| 문서구분 | `docSeComCd` | string | 코드명 표시 — 공통코드 (TBD 상위코드) |
| 필수첨부파일명 | `esntlAtchFileNm` | string | |
| 필수첨부여부 | `esntlAtchYn` | string | `Y`→예, `N`→아니오 |
| 필수파일검토내용 | `esntlAtchFileRvwCn` | string | 없으면 `—` 표시, 최대 2줄 clamp |
| 비고 | `esntlAtchFileRmrk` | string | 없으면 `—` 표시 |
| 양식 내려받기 | `atchFileGroupId` | string | 양식파일 다운로드 버튼 |
| 관리 | — | — | 행별 **[수정] · [삭제]** |
- 행 클릭 시 별도 상세 화면 없음. 편집은 **[수정]** 버튼으로 다이얼로그 C를 연다.
@@ -167,13 +169,15 @@ END
### 6-1. 폼 필드
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
| ------------------ | ---------- | ---- | ------ | ------------------- | -------------------------------- |
| 필수첨부파일아이디 | (Readonly) | Y | — | `esntlAtchFileId` | 등록 시 미노출 |
| 문서구분 | CodeSelect | Y | — | `docSeComCd` | 예: 설치인가, 기본계획 |
| 필수첨부파일명 | Text | Y | — | `esntlAtchFileNm` | placeholder: "예: 사업계획서" |
| 필수첨부여부 | RadioGroup | Y | Y | `esntlAtchYn` | Y/N, UI 표시: 예/아니오 |
| 비고 | Textarea | N | — | `esntlAtchFileRmrk` | placeholder: "비고를 입력하세요" |
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
| ------------------ | ------------------- | ---- | ------ | -------------------- | ---------------------------------- |
| 필수첨부파일아이디 | (Readonly) | Y | — | `esntlAtchFileId` | 등록 시 미노출 |
| 문서구분 | CodeSelect | Y | — | `docSeComCd` | 예: 설치인가, 기본계획 |
| 필수첨부파일명 | Text | Y | — | `esntlAtchFileNm` | placeholder: "예: 사업계획서" |
| 양식파일 | AttachmentFileField | N | — | `atchFileGroupId` | TUS 파일 업로드 |
| 필수첨부여부 | RadioGroup | Y | Y | `esntlAtchYn` | Y/N, UI 표시: 예/아니오 |
| 필수파일검토내용 | Textarea | N | — | `esntlAtchFileRvwCn` | placeholder: "검토내용을 입력하세요"|
| 비고 | Textarea | N | — | `esntlAtchFileRmrk` | placeholder: "비고를 입력하세요" |
### 6-2. 버튼
@@ -210,28 +214,32 @@ END
**응답 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? | 수정일시 |
| Field | 타입 | 설명 |
| -------------------- | -------------------------- | -------------------------------- |
| `esntlAtchFileId` | string | 필수첨부파일아이디 |
| `docSeComCd` | string | 문서구분공통코드 |
| `esntlAtchFileNm` | string | 필수첨부파일명 |
| `esntlAtchYn` | string | 필수첨부여부 (Y/N) |
| `esntlAtchFileRvwCn` | string? | 필수첨부파일검토내용 |
| `esntlAtchFileRmrk` | string? | 필수첨부파일비고 |
| `atchFileGroupId` | string? | 첨부파일그룹아이디 (양식파일) |
| `atchFileList` | AtflMngClientResponseDto[]?| 첨부파일 목록 |
| `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 | 비고 |
| Field | 필수 | 설명 |
| -------------------- | ---- | -------------------- |
| `docSeComCd` | Y | 문서구분 |
| `esntlAtchFileNm` | N | 필수첨부파일명 |
| `esntlAtchYn` | N | 필수첨부여부 |
| `esntlAtchFileRvwCn` | N | 필수첨부파일검토내용 |
| `esntlAtchFileRmrk` | N | 비고 |
| `atchFileGroupId` | N | 첨부파일그룹아이디 |
**검색 Query**
+14
View File
@@ -2,6 +2,12 @@ import type { NextConfig } from "next";
const subDomain = process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13000";
const webhwpCtrlBase = (
process.env.NEXT_PUBLIC_WEBHYP_CTRL_URL || "https://hwp.onasys.co.kr/webhwpctrl"
).replace(/\/$/, "");
const webhwpAssetPrefixes = ["js", "hwpsdk", "dist", "commonFrame", "wasm"] as const;
const nextConfig: NextConfig = {
output: "standalone",
reactStrictMode: true,
@@ -30,6 +36,14 @@ const nextConfig: NextConfig = {
source: "/cms/:path*",
destination: `${subDomain}/cms/:path*`,
},
...webhwpAssetPrefixes.map((prefix) => ({
source: `/webhwpctrl/${prefix}/:path*`,
destination: `${webhwpCtrlBase}/${prefix}/:path*`,
})),
{
source: "/webhwpctrl/file/:path*",
destination: `${webhwpCtrlBase}/file/:path*`,
},
],
fallback: [],
};
+3 -2
View File
@@ -3,9 +3,9 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3003",
"dev": "next dev -p 3004",
"build": "next build",
"start": "next start -p 3003",
"start": "next start -p 3004",
"lint": "next lint",
"lint:fsd": "steiger ."
},
@@ -36,6 +36,7 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@hookform/devtools": "^4.4.0",
"@types/js-cookie": "^3.0.6",
"eslint": "^9.0.0",
"eslint-config-next": "15.3.8",
+63
View File
@@ -0,0 +1,63 @@
{
"name": "as",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3004",
"build": "next build",
"start": "next start -p 3004",
"lint": "next lint",
"lint:fsd": "steiger .",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:e2e": "playwright test"
},
"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",
"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": {
"@eslint/eslintrc": "^3",
"@hookform/devtools": "^4.4.0",
"@playwright/test": "^1.61.1",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.3",
"@types/js-cookie": "^3.0.6",
"@vitejs/plugin-react": "^4.7.0",
"@vitejs/plugin-react-oxc": "^0.4.3",
"eslint": "^9.0.0",
"eslint-config-next": "15.3.8",
"eslint-plugin-react-hooks": "^5.0.0",
"jsdom": "^30.1.2",
"postcss": "^8.4.49",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11",
"steiger": "^0.5.11",
"tailwindcss": "^3.4.14",
"typescript": "^5.7.2",
"vite": "^8.3.3",
"vitest": "^5.0.3"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

File diff suppressed because one or more lines are too long
+91
View File
@@ -0,0 +1,91 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
/**
* Jquery integration plugin.
*
* @class tinymce.core.JqueryIntegration
* @private
*/
!function(){function f(){
// Reference to tinymce needs to be lazily evaluated since tinymce
// might be loaded through the compressor or other means
return d.tinymce}var p,c,u,s=[],d="undefined"!=typeof global?global:window,m=d.jQuery;m.fn.tinymce=function(o){var e,t,i,n,l=this,r="";
// No match then just ignore the call
return l.length?
// Get editor instance
o?(l.css("visibility","hidden"),
// Load TinyMCE on demand, if we need to
d.tinymce||c||!(e=o.script_url)?
// Delay the init call until tinymce is loaded
1===c?s.push(a):a():(c=1,t=e.substring(0,e.lastIndexOf("/")),
// Check if it's a dev/src version they want to load then
// make sure that all plugins, themes etc are loaded in source mode as well
-1!=e.indexOf(".min")&&(r=".min"),
// Setup tinyMCEPreInit object this will later be used by the TinyMCE
// core script to locate other resources like CSS files, dialogs etc
// You can also predefined a tinyMCEPreInit object and then it will use that instead
d.tinymce=d.tinyMCEPreInit||{base:t,suffix:r},
// url contains gzip then we assume it's a compressor
-1!=e.indexOf("gzip")&&(i=o.language||"en",e=e+(/\?/.test(e)?"&":"?")+"js=true&core=true&suffix="+escape(r)+"&themes="+escape(o.theme||"modern")+"&plugins="+escape(o.plugins||"")+"&languages="+(i||""),
// Check if compressor script is already loaded otherwise setup a basic one
d.tinyMCE_GZ||(d.tinyMCE_GZ={start:function(){function n(e){f().ScriptLoader.markDone(f().baseURI.toAbsolute(e))}
// Add core languages
n("langs/"+i+".js"),
// Add themes with languages
n("themes/"+o.theme+"/theme"+r+".js"),n("themes/"+o.theme+"/langs/"+i+".js"),
// Add plugins with languages
m.each(o.plugins.split(","),function(e,t){t&&(n("plugins/"+t+"/plugin"+r+".js"),n("plugins/"+t+"/langs/"+i+".js"))})},end:function(){}})),(n=document.createElement("script")).type="text/javascript",n.onload=n.onreadystatechange=function(e){e=e||window.event,2===c||"load"!=e.type&&!/complete|loaded/.test(n.readyState)||(f().dom.Event.domLoaded=1,c=2,
// Execute callback after mainscript has been loaded and before the initialization occurs
o.script_loaded&&o.script_loaded(),a(),m.each(s,function(e,t){t()}))},n.src=e,document.body.appendChild(n)),l):f()?f().get(l[0].id):null:l;function a(){var a=[],c=0;
// Apply patches to the jQuery object, only once
u||(v(),u=!0),
// Create an editor instance for each matched node
l.each(function(e,t){var n,i=t.id,r=o.oninit;
// Generate unique id for target element if needed
i||(t.id=i=f().DOM.uniqueId()),
// Only init the editor once
f().get(i)||(
// Create editor instance and render it
n=f().createEditor(i,o),a.push(n),n.on("init",function(){var e,t=r;l.css("visibility",""),
// Run this if the oninit setting is defined
// this logic will fire the oninit callback ones each
// matched editor instance is initialized
r&&++c==a.length&&("string"==typeof t&&(e=-1===t.indexOf(".")?null:f().resolve(t.replace(/\.\w+$/,"")),t=f().resolve(t)),
// Call the oninit function with the object
t.apply(e||f(),a))}))}),
// Render the editor instances in a separate loop since we
// need to have the full editors array used in the onInit calls
m.each(a,function(e,t){t.render()})}},
// Add :tinymce pseudo selector this will select elements that has been converted into editor instances
// it's now possible to use things like $('*:tinymce') to get all TinyMCE bound elements.
m.extend(m.expr[":"],{tinymce:function(e){var t;return!!(e.id&&"tinymce"in d&&(t=f().get(e.id))&&t.editorManager===f())}});
// This function patches internal jQuery functions so that if
// you for example remove an div element containing an editor it's
// automatically destroyed by the TinyMCE API
var v=function(){function r(e){
// If the function is remove
"remove"===e&&this.each(function(e,t){var n=u(t);n&&n.remove()}),this.find("span.mceEditor,div.mceEditor").each(function(e,t){var n=f().get(t.id.replace(/_parent$/,""));n&&n.remove()})}function o(i){var e,t=this;
// Handle set value
/*jshint eqnull:true */if(null!=i)r.call(t),
// Saves the contents before get/set value of textarea/div
t.each(function(e,t){var n;(n=f().get(t.id))&&n.setContent(i)});else if(0<t.length&&(e=f().get(t[0].id)))return e.getContent()}function l(e){return e&&e.length&&d.tinymce&&e.is(":tinymce")}
// Removes any child editor instances by looking for editor wrapper elements
var u=function(e){var t=null;return e&&e.id&&d.tinymce?f().get(e.id):t},s={};
// Loads or saves contents from/to textarea if the value
// argument is defined it will set the TinyMCE internal contents
// Patch some setter/getter functions these will
// now be able to set/get the contents of editor instances for
// example $('#editorid').html('Content'); will update the TinyMCE iframe instance
m.each(["text","html","val"],function(e,t){var a=s[t]=m.fn[t],c="text"===t;m.fn[t]=function(e){var t=this;if(!l(t))return a.apply(t,arguments);if(e!==p)return o.call(t.filter(":tinymce"),e),a.apply(t.not(":tinymce"),arguments),t;// return original set for chaining
var i="",r=arguments;return(c?t:t.eq(0)).each(function(e,t){var n=u(t);i+=n?c?n.getContent().replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g,""):n.getContent({save:!0}):a.apply(m(t),r)}),i}}),
// Makes it possible to use $('#id').append("content"); to append contents to the TinyMCE editor iframe
m.each(["append","prepend"],function(e,t){var n=s[t]=m.fn[t],r="prepend"===t;m.fn[t]=function(i){var e=this;return l(e)?i!==p?("string"==typeof i&&e.filter(":tinymce").each(function(e,t){var n=u(t);n&&n.setContent(r?i+n.getContent():n.getContent()+i)}),n.apply(e.not(":tinymce"),arguments),e):void 0:n.apply(e,arguments)}}),
// Makes sure that the editor instance gets properly destroyed when the parent element is removed
m.each(["remove","replaceWith","replaceAll","empty"],function(e,t){var n=s[t]=m.fn[t];m.fn[t]=function(){return r.call(this,t),n.apply(this,arguments)}}),s.attr=m.fn.attr,
// Makes sure that $('#tinymce_id').attr('value') gets the editors current HTML contents
m.fn.attr=function(e,t){var n=this,i=arguments;if(!e||"value"!==e||!l(n))return s.attr.apply(n,i);if(t!==p)return o.call(n.filter(":tinymce"),t),s.attr.apply(n.not(":tinymce"),i),n;// return original set for chaining
var r=n[0],a=u(r);return a?a.getContent({save:!0}):s.attr.apply(m(r),i)}}}();
+3
View File
@@ -0,0 +1,3 @@
This is where language files should be placed.
Please DO NOT translate these directly use this service: https://www.transifex.com/projects/p/tinymce/
File diff suppressed because one or more lines are too long
+504
View File
@@ -0,0 +1,504 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!
+9
View File
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";function f(t,e,n){t.execCommand("UL"===e?"InsertUnorderedList":"InsertOrderedList",!1,!1===n?null:{"list-style-type":n})}function i(t){return function(){return t}}function t(t){return t}function e(){return s}var n=tinymce.util.Tools.resolve("tinymce.PluginManager"),g=tinymce.util.Tools.resolve("tinymce.util.Tools"),u=i(!1),l=i(!0),s={fold:function(t,e){return t()},isSome:u,isNone:l,getOr:t,getOrThunk:r,getOrDie:function(t){throw new Error(t||"error: getOrDie called on none.")},getOrNull:i(null),getOrUndefined:i(void 0),or:t,orThunk:r,map:e,each:function(){},bind:e,exists:u,forall:l,filter:function(){return s},toArray:function(){return[]},toString:i("none()")};function r(t){return t()}function d(t){return t&&/^(TH|TD)$/.test(t.nodeName)}function m(c,a){return function(s){function t(t){var e,n,r,o,i,u,l;s.setActive((e=c,r=a,i=-1!==(o=function(t,e){for(var n=0;n<t.length;n++)if(e(t[n]))return n;return-1}((n=t).parents,d))?n.parents.slice(0,o):n.parents,0<(u=g.grep(i,(l=e,function(t){return t&&/^(OL|UL|DL)$/.test(t.nodeName)&&(e=t,l.$.contains(l.getBody(),e));var e}))).length&&u[0].nodeName===r))}return c.on("NodeChange",t),function(){return c.off("NodeChange",t)}}}function c(t,e,n,r,o,i){var u,l,s,c,a,d;1<i.length?(c=r,a=o,d=i,(s=t).ui.registry.addSplitButton(e,{tooltip:n,icon:"OL"===a?"ordered-list":"unordered-list",presets:"listpreview",columns:3,fetch:function(t){t(g.map(d,function(t){return{type:"choiceitem",value:"default"===t?"":t,icon:"list-"+("OL"===a?"num":"bull")+"-"+("disc"===t||"decimal"===t?"default":t),text:t.replace(/\-/g," ").replace(/\b\w/g,function(t){return t.toUpperCase()})}}))},onAction:function(){return s.execCommand(c)},onItemAction:function(t,e){f(s,a,e)},select:function(e){var t,n=(t=s).dom.getParent(t.selection.getNode(),"ol,ul"),r=t.dom.getStyle(n,"listStyleType");return p(r).map(function(t){return e===t}).getOr(!1)},onSetup:m(s,a)})):(l=r,(u=t).ui.registry.addToggleButton(e,{active:!1,tooltip:n,icon:"OL"===o?"ordered-list":"unordered-list",onSetup:m(u,o),onAction:function(){return u.execCommand(l)}}))}var a=function(n){function t(){return o}function e(t){return t(n)}var r=i(n),o={fold:function(t,e){return e(n)},isSome:l,isNone:u,getOr:r,getOrThunk:r,getOrDie:r,getOrNull:r,getOrUndefined:r,or:t,orThunk:t,map:function(t){return a(t(n))},each:function(t){t(n)},bind:e,exists:e,forall:e,filter:function(t){return t(n)?o:s},toArray:function(){return[n]},toString:function(){return"some("+n+")"}};return o},p=function(t){return null==t?s:a(t)};n.add("advlist",function(t){var n,e,r,o;t.hasPlugin("lists")?(c(e=t,"numlist","Numbered list","InsertOrderedList","OL",(r=e.getParam("advlist_number_styles","default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman"))?r.split(/[ ,]/):[]),c(e,"bullist","Bullet list","InsertUnorderedList","UL",(o=e.getParam("advlist_bullet_styles","default,circle,square"))?o.split(/[ ,]/):[]),(n=t).addCommand("ApplyUnorderedListStyle",function(t,e){f(n,"UL",e["list-style-type"])}),n.addCommand("ApplyOrderedListStyle",function(t,e){f(n,"OL",e["list-style-type"])})):console.error("Please use the Lists plugin together with the Advanced List plugin.")})}();
+9
View File
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";function a(e){return e.getAttribute("id")||e.getAttribute("name")||""}function c(e){return e&&"a"===e.nodeName.toLowerCase()&&!e.getAttribute("href")&&""!==a(e)}function d(e){return e.dom.getParent(e.selection.getStart(),l)}function r(e,t){var o,a,n,r,i,l=d(e);l?(n=e,r=t,(i=l).removeAttribute("name"),i.id=r,n.addVisual(),n.undoManager.add()):(a=t,(o=e).undoManager.transact(function(){var e,n;o.getParam("allow_html_in_named_anchor",!1,"boolean")||o.selection.collapse(!0),o.selection.isCollapsed()?o.insertContent(o.dom.createHTML("a",{id:a})):(n=(e=o).dom,u(n).walk(e.selection.getRng(),function(e){s.each(e,function(e){var t;c(t=e)&&!t.firstChild&&n.remove(e,!1)})}),o.formatter.remove("namedAnchor",null,null,!0),o.formatter.apply("namedAnchor",{value:a}),o.addVisual())})),e.focus()}function i(r){return function(e){for(var t,n=0;n<e.length;n++){var o=e[n],a=void 0;!(a=t=o)||a.attr("href")||!a.attr("id")&&!a.attr("name")||t.firstChild||o.attr("contenteditable",r)}}}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),u=tinymce.util.Tools.resolve("tinymce.dom.RangeUtils"),s=tinymce.util.Tools.resolve("tinymce.util.Tools"),l="a:not([href])";e.add("anchor",function(e){var t,n,o;(t=e).on("PreInit",function(){t.parser.addNodeFilter("a",i("false")),t.serializer.addNodeFilter("a",i(null))}),(n=e).addCommand("mceAnchor",function(){var o,e,t;t=(e=d(o=n))?a(e):"",o.windowManager.open({title:"Anchor",size:"normal",body:{type:"panel",items:[{name:"id",type:"input",label:"ID",placeholder:"example"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{id:t},onSubmit:function(e){var t=o,n=e.getData().id;(/^[A-Za-z][A-Za-z0-9\-:._]*$/.test(n)?(r(t,n),0):(t.windowManager.alert("Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores."),1))||e.close()}})}),(o=e).ui.registry.addToggleButton("anchor",{icon:"bookmark",tooltip:"Anchor",onAction:function(){return o.execCommand("mceAnchor")},onSetup:function(e){return o.selection.selectorChangedWithUnbind("a:not([href])",e.setActive).unbind}}),o.ui.registry.addMenuItem("anchor",{icon:"bookmark",text:"Anchor...",onAction:function(){return o.execCommand("mceAnchor")}}),e.on("PreInit",function(){e.formatter.register("namedAnchor",{inline:"a",selector:l,remove:"all",split:!0,deep:!0,attributes:{id:"%value"},onmatch:c})})})}();
+9
View File
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";function k(e){return/^[(\[{ \u00a0]$/.test(e)}function w(e){return 3===e.nodeType}function i(e){return 1===e.nodeType}function o(e,t){var n;return t<0&&(t=0),!w(e)||(n=e.data.length)<t&&(t=n),t}function y(e,t,n){!i(t)||t.hasChildNodes()?e.setStart(t,o(t,n)):e.setStartBefore(t)}function v(e,t,n){!i(t)||t.hasChildNodes()?e.setEnd(t,o(t,n)):e.setEndAfter(t)}function r(e,t){var n,i,o,r,a,f=e.getParam("autolink_pattern",A),s=e.getParam("default_link_target",!1);if(null===e.dom.getParent(e.selection.getNode(),"a[href]")){var d=e.selection.getRng().cloneRange();if(d.startOffset<5){if(!(r=d.endContainer.previousSibling)){if(!d.endContainer.firstChild||!d.endContainer.firstChild.nextSibling)return;r=d.endContainer.firstChild.nextSibling}if(y(d,r,a=r.length),v(d,r,a),d.endOffset<5)return;n=d.endOffset,i=r}else{if(!w(i=d.endContainer)&&i.firstChild){for(;!w(i)&&i.firstChild;)i=i.firstChild;w(i)&&(y(d,i,0),v(d,i,i.nodeValue.length))}n=1===d.endOffset?2:d.endOffset-1-t}for(var l=n;y(d,i,2<=n?n-2:0),v(d,i,1<=n?n-1:0),--n,!k(d.toString())&&0<=n-2;);k(d.toString())?(y(d,i,n),v(d,i,l),n+=1):(0===d.startOffset?y(d,i,0):y(d,i,n),v(d,i,l)),u=d.toString(),/[?!,.;:]/.test(u.charAt(u.length-1))&&v(d,i,l-1);var u,c,g,h,C=(u=d.toString().trim()).match(f),m=e.getParam("link_default_protocol","http","string");C&&((g=c=C[0]).length>=(h="www.").length&&g.substr(0,0+h.length)===h?c=m+"://"+c:-1===c.indexOf("@")||/^([A-Za-z][A-Za-z\d.+-]*:\/\/)|mailto:/.test(c)||(c="mailto:"+c),o=e.selection.getBookmark(),e.selection.setRng(d),e.execCommand("createlink",!1,c),!1!==s&&e.dom.setAttrib(e.selection.getNode(),"target",s),e.selection.moveToBookmark(o),e.nodeChanged())}}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=tinymce.util.Tools.resolve("tinymce.Env"),A=new RegExp("^"+/(?:[A-Za-z][A-Za-z\d.+-]{0,14}:\/\/(?:[-.~*+=!&;:'%@?^${}(),\w]+@)?|www\.|[-;:&=+$,.\w]+@)[A-Za-z\d-]+(?:\.[A-Za-z\d-]+)*(?::\d+)?(?:\/(?:[-.~*+=!;:'%@$(),\/\w]*[-~*+=%@$()\/\w])?)?(?:\?(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?(?:#(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?/g.source+"$","i");e.add("autolink",function(e){var t,n;(t=e).on("keydown",function(e){if(13===e.keyCode)return r(t,-1)}),a.browser.isIE()?t.on("focus",function(){if(!n){n=!0;try{t.execCommand("AutoUrlDetect",!1,!0)}catch(e){}}}):(t.on("keypress",function(e){if(41===e.keyCode||93===e.keyCode||125===e.keyCode)return r(t,-1)}),t.on("keyup",function(e){if(32===e.keyCode)return r(t,0)}))})}();
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";function y(e){return e.getParam("min_height",e.getElement().offsetHeight,"number")}function p(e,t){var n=e.getBody();n&&(n.style.overflowY=t?"":"hidden",t||(n.scrollTop=0))}function v(e,t,n,i){var o=parseInt(e.getStyle(t,n,i),10);return isNaN(o)?0:o}var l=Object.hasOwnProperty,e=tinymce.util.Tools.resolve("tinymce.PluginManager"),b=tinymce.util.Tools.resolve("tinymce.Env"),r=tinymce.util.Tools.resolve("tinymce.util.Delay"),u=function(e,t,n,i,o){r.setEditorTimeout(e,function(){C(e,t),n--?u(e,t,n,i,o):o&&o()},i)},C=function(e,t,n){var i,o,r,s,a,l,u,g,c,m,f,d=e.dom,h=e.getDoc();h&&(e.plugins.fullscreen&&e.plugins.fullscreen.isFullscreen()?p(e,!0):(i=h.documentElement,o=e.getParam("autoresize_bottom_margin",50,"number"),r=y(e),s=v(d,i,"margin-top",!0),a=v(d,i,"margin-bottom",!0),(l=(l=i.offsetHeight+s+a+o)<0?0:l)+(u=e.getContainer().offsetHeight-e.getContentAreaContainer().offsetHeight)>y(e)&&(r=l+u),(g=e.getParam("max_height",0,"number"))&&g<r?(r=g,p(e,!0)):p(e,!1),r!==t.get()&&(c=r-t.get(),d.setStyle(e.getContainer(),"height",r+"px"),t.set(r),e.fire("ResizeEditor"),b.browser.isSafari()&&b.mac&&(m=e.getWin()).scrollTo(m.pageXOffset,m.pageYOffset),!e.hasFocus()||"setcontent"!==(null==(f=n)?void 0:f.type.toLowerCase())||!0!==f.selection&&!0!==f.paste||e.selection.scrollIntoView(),b.webkit&&c<0&&C(e,t,n))))};e.add("autoresize",function(e){var t,n,i,o,r,s,a=e.settings;l.call(a,"resize")||(e.settings.resize=!1),e.inline||(s=0,r=t={get:function(){return s},set:function(e){s=e}},(o=e).addCommand("mceAutoResize",function(){C(o,r)}),i=t,(n=e).on("init",function(){var e=n.getParam("autoresize_overflow_padding",1,"number"),t=n.dom;t.setStyles(n.getDoc().documentElement,{height:"auto"}),t.setStyles(n.getBody(),{paddingLeft:e,paddingRight:e,"min-height":0})}),n.on("NodeChange SetContent keyup FullscreenStateChanged ResizeContent",function(e){C(n,i,e)}),n.getParam("autoresize_on_init",!0,"boolean")&&n.on("init",function(){u(n,i,20,100,function(){u(n,i,5,1e3)})}))})}();
+9
View File
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";function o(t,e){var r=t||e,n=/^(\d+)([ms]?)$/.exec(""+r);return(n[2]?{s:1e3,m:6e4}[n[2]]:1)*parseInt(r,10)}function n(t){var e=document.location;return t.getParam("autosave_prefix","tinymce-autosave-{path}{query}{hash}-{id}-").replace(/{path}/g,e.pathname).replace(/{query}/g,e.search).replace(/{hash}/g,e.hash).replace(/{id}/g,t.id)}function i(t,e){if(a(e))return t.dom.isEmpty(t.getBody());var r=d.trim(e);if(""===r)return!0;var n=(new DOMParser).parseFromString(r,"text/html");return t.dom.isEmpty(n)}function u(t){var e=parseInt(v.getItem(n(t)+"time"),10)||0;return!((new Date).getTime()-e>o(t.getParam("autosave_retention"),"20m")&&(g(t,!1),1))}function s(t){var e=n(t);!i(t)&&t.isDirty()&&(v.setItem(e+"draft",t.getContent({format:"raw",no_events:!0})),v.setItem(e+"time",(new Date).getTime().toString()),t.fire("StoreDraft"))}function f(t){var e=n(t);u(t)&&(t.setContent(v.getItem(e+"draft"),{format:"raw"}),t.fire("RestoreDraft"))}function c(t){t.undoManager.transact(function(){f(t),g(t)}),t.focus()}function m(r){return function(t){function e(){return t.setDisabled(!u(r))}return t.setDisabled(!u(r)),r.on("StoreDraft RestoreDraft RemoveDraft",e),function(){return r.off("StoreDraft RestoreDraft RemoveDraft",e)}}}var t=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=function(t){return void 0===t},l=tinymce.util.Tools.resolve("tinymce.util.Delay"),v=tinymce.util.Tools.resolve("tinymce.util.LocalStorage"),d=tinymce.util.Tools.resolve("tinymce.util.Tools"),g=function(t,e){var r=n(t);v.removeItem(r+"draft"),v.removeItem(r+"time"),!1!==e&&t.fire("RemoveDraft")},y=tinymce.util.Tools.resolve("tinymce.EditorManager");t.add("autosave",function(t){var e,r,n,a;return t.editorManager.on("BeforeUnload",function(t){var e;d.each(y.get(),function(t){t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&t.getParam("autosave_ask_before_unload",!0)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))}),e&&(t.preventDefault(),t.returnValue=e)}),n=e=t,a=o(n.getParam("autosave_interval"),"30s"),l.setEditorInterval(n,function(){s(n)},a),e.ui.registry.addButton("restoredraft",{tooltip:"Restore last draft",icon:"restore-draft",onAction:function(){c(e)},onSetup:m(e)}),e.ui.registry.addMenuItem("restoredraft",{text:"Restore last draft",icon:"restore-draft",onAction:function(){c(e)},onSetup:m(e)}),t.on("init",function(){t.getParam("autosave_restore_when_empty",!1)&&t.dom.isEmpty(t.getBody())&&f(t)}),r=t,{hasDraft:function(){return u(r)},storeDraft:function(){return s(r)},restoreDraft:function(){return f(r)},removeDraft:function(t){return g(r,t)},isEmpty:function(t){return i(r,t)}}})}();
+9
View File
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";function i(t){function o(o,e){t=t.replace(o,e)}return t=n.trim(t),o(/\n/gi,"<br />"),o(/\[b\]/gi,"<strong>"),o(/\[\/b\]/gi,"</strong>"),o(/\[i\]/gi,"<em>"),o(/\[\/i\]/gi,"</em>"),o(/\[u\]/gi,"<u>"),o(/\[\/u\]/gi,"</u>"),o(/\[url=([^\]]+)\](.*?)\[\/url\]/gi,'<a href="$1">$2</a>'),o(/\[url\](.*?)\[\/url\]/gi,'<a href="$1">$1</a>'),o(/\[img\](.*?)\[\/img\]/gi,'<img src="$1" />'),o(/\[color=(.*?)\](.*?)\[\/color\]/gi,'<font color="$1">$2</font>'),o(/\[code\](.*?)\[\/code\]/gi,'<span class="codeStyle">$1</span>&nbsp;'),o(/\[quote.*?\](.*?)\[\/quote\]/gi,'<span class="quoteStyle">$1</span>&nbsp;'),t}var o=tinymce.util.Tools.resolve("tinymce.PluginManager"),n=tinymce.util.Tools.resolve("tinymce.util.Tools");o.add("bbcode",function(o){o.on("BeforeSetContent",function(o){o.content=i(o.content)}),o.on("PostProcess",function(o){function e(o,e){t=t.replace(o,e)}var t;o.set&&(o.content=i(o.content)),o.get&&(o.content=(t=o.content,t=n.trim(t),e(/<a.*?href=\"(.*?)\".*?>(.*?)<\/a>/gi,"[url=$1]$2[/url]"),e(/<font.*?color=\"(.*?)\".*?class=\"codeStyle\".*?>(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),e(/<font.*?color=\"(.*?)\".*?class=\"quoteStyle\".*?>(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),e(/<font.*?class=\"codeStyle\".*?color=\"(.*?)\".*?>(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),e(/<font.*?class=\"quoteStyle\".*?color=\"(.*?)\".*?>(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),e(/<span style=\"color: ?(.*?);\">(.*?)<\/span>/gi,"[color=$1]$2[/color]"),e(/<font.*?color=\"(.*?)\".*?>(.*?)<\/font>/gi,"[color=$1]$2[/color]"),e(/<span style=\"font-size:(.*?);\">(.*?)<\/span>/gi,"[size=$1]$2[/size]"),e(/<font>(.*?)<\/font>/gi,"$1"),e(/<img.*?src=\"(.*?)\".*?\/>/gi,"[img]$1[/img]"),e(/<span class=\"codeStyle\">(.*?)<\/span>/gi,"[code]$1[/code]"),e(/<span class=\"quoteStyle\">(.*?)<\/span>/gi,"[quote]$1[/quote]"),e(/<strong class=\"codeStyle\">(.*?)<\/strong>/gi,"[code][b]$1[/b][/code]"),e(/<strong class=\"quoteStyle\">(.*?)<\/strong>/gi,"[quote][b]$1[/b][/quote]"),e(/<em class=\"codeStyle\">(.*?)<\/em>/gi,"[code][i]$1[/i][/code]"),e(/<em class=\"quoteStyle\">(.*?)<\/em>/gi,"[quote][i]$1[/i][/quote]"),e(/<u class=\"codeStyle\">(.*?)<\/u>/gi,"[code][u]$1[/u][/code]"),e(/<u class=\"quoteStyle\">(.*?)<\/u>/gi,"[quote][u]$1[/u][/quote]"),e(/<\/(strong|b)>/gi,"[/b]"),e(/<(strong|b)>/gi,"[b]"),e(/<\/(em|i)>/gi,"[/i]"),e(/<(em|i)>/gi,"[i]"),e(/<\/u>/gi,"[/u]"),e(/<span style=\"text-decoration: ?underline;\">(.*?)<\/span>/gi,"[u]$1[/u]"),e(/<u>/gi,"[u]"),e(/<blockquote[^>]*>/gi,"[quote]"),e(/<\/blockquote>/gi,"[/quote]"),e(/<br \/>/gi,"\n"),e(/<br\/>/gi,"\n"),e(/<br>/gi,"\n"),e(/<p>/gi,""),e(/<\/p>/gi,"\n"),e(/&nbsp;|\u00a0/gi," "),e(/&quot;/gi,'"'),e(/&lt;/gi,"<"),e(/&gt;/gi,">"),e(/&amp;/gi,"&"),t))})})}();
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("code",function(e){var t,o;function n(){return o.execCommand("mceCodeEditor")}return(t=e).addCommand("mceCodeEditor",function(){var n,e;e=(n=t).getContent({source_view:!0}),n.windowManager.open({title:"Source Code",size:"large",body:{type:"panel",items:[{type:"textarea",name:"code"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{code:e},onSubmit:function(e){var t=n,o=e.getData().code;t.focus(),t.undoManager.transact(function(){t.setContent(o)}),t.selection.setCursorLocation(),t.nodeChanged(),e.close()}})}),(o=e).ui.registry.addButton("code",{icon:"sourcecode",tooltip:"Source code",onAction:n}),o.ui.registry.addMenuItem("code",{icon:"sourcecode",text:"Source code",onAction:n}),{}})}();
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("colorpicker",function(){})}();
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("contextmenu",function(){})}();
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("hr",function(n){var o,t;function e(){return t.execCommand("InsertHorizontalRule")}(o=n).addCommand("InsertHorizontalRule",function(){o.execCommand("mceInsertContent",!1,"<hr />")}),(t=n).ui.registry.addButton("hr",{icon:"horizontal-rule",tooltip:"Horizontal line",onAction:e}),t.ui.registry.addMenuItem("hr",{icon:"horizontal-rule",text:"Horizontal line",onAction:e})})}();
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";function t(n){return function(t){return r=typeof(e=t),(null===e?"null":"object"==r&&(Array.prototype.isPrototypeOf(e)||e.constructor&&"Array"===e.constructor.name)?"array":"object"==r&&(String.prototype.isPrototypeOf(e)||e.constructor&&"String"===e.constructor.name)?"string":r)===n;var e,r}}function y(t){return t.getParam("importcss_selector_converter")}function u(e){return l(e)?function(t){return-1!==t.indexOf(e)}:e instanceof RegExp?function(t){return e.test(t)}:e}function h(t,e){var r,n=/^(?:([a-z0-9\-_]+))?(\.[a-z0-9_\-\.]+)$/i.exec(e);if(n){var o=n[1],i=n[2].substr(1).split(".").join(" "),c=x.makeMap("a,img");return n[1]?(r={title:e},t.schema.getTextBlockElements()[o]?r.block=o:t.schema.getBlockElements()[o]||c[o.toLowerCase()]?r.selector=o:r.inline=o):n[2]&&(r={inline:"span",title:e.substr(1),classes:i}),!1!==t.getParam("importcss_merge_classes")?r.classes=i:r.attributes={class:i},r}}function d(t,e){return null===e||!1!==t.getParam("importcss_exclusive")}function r(v){v.on("init",function(){function o(t,e){if(f=t,p=g,!(d(v,m=e)?f in p:f in m.selectors)){a=t,l=g,d(v,u=e)?l[a]=!0:u.selectors[a]=!0;var r=(i=(o=v).plugins.importcss,c=t,((s=e)&&s.selector_converter?s.selector_converter:y(o)?y(o):function(){return h(o,c)}).call(i,c,s));if(r){var n=r.name||_.DOM.uniqueId();return v.formatter.register(n,r),{title:r.title,format:n}}}var o,i,c,s,a,u,l,f,m,p;return null}var e,r,n,t,i=(e=[],r=[],n={},{addItemToGroup:function(t,e){n[t]?n[t].push(e):(r.push(t),n[t]=[e])},addItem:function(t){e.push(t)},toFormats:function(){return function(t){for(var e=[],r=0,n=t.length;r<n;++r){if(!f(t[r]))throw new Error("Arr.flatten item "+r+" was not an array, input: "+t);P.apply(e,t[r])}return e}(function(t,e){for(var r=t.length,n=new Array(r),o=0;o<r;o++){var i=t[o];n[o]=e(i,o)}return n}(r,function(t){var e=n[t];return 0===e.length?[]:[{title:t,items:e}]})).concat(e)}}),g={},c=u(v.getParam("importcss_selector_filter")),s=(t=v.getParam("importcss_groups"),x.map(t,function(t){return x.extend({},t,{original:t,selectors:{},filter:u(t.filter)})}));x.each(function(c,t,s){var a=[],r={},u=function(t,e){var r,n,o=n=t.href,i=p.cacheSuffix;if((n=o=l(o)?o.replace("?"+i,"").replace("&"+i,""):o)&&s(n,e)&&!function(t,e){var r,n=!1!==(r=t.getParam("skin"))&&(r||"oxide");if(n){var o=t.getParam("skin_url"),i=o?t.documentBaseURI.toAbsolute(o):m.baseURL+"/skins/ui/"+n,c=m.baseURL+"/skins/content/";return e===i+"/content"+(t.inline?".inline":"")+".min.css"||-1!==e.indexOf(c)}}(c,n)){x.each(t.imports,function(t){u(t,!0)});try{r=t.cssRules||t.rules}catch(t){}x.each(r,function(t){t.styleSheet?u(t.styleSheet,!0):t.selectorText&&x.each(t.selectorText.split(","),function(t){a.push(x.trim(t))})})}};x.each(c.contentCSS,function(t){r[t]=!0}),s=s||function(t,e){return e||r[t]};try{x.each(t.styleSheets,function(t){u(t)})}catch(t){}return a}(v,v.getDoc(),u(v.getParam("importcss_file_filter"))),function(r){var t,e,n;T.test(r)||c&&!c(r)||(n=r,0<(t=x.grep(s,function(t){return!t.filter||t.filter(n)})).length?x.each(t,function(t){var e=o(r,t);e&&i.addItemToGroup(t.title,e)}):(e=o(r,null))&&i.addItem(e))});var a=i.toFormats();v.fire("addStyleModifications",{items:a,replace:!v.getParam("importcss_append")})})}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),l=t("string"),f=t("array"),_=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),m=tinymce.util.Tools.resolve("tinymce.EditorManager"),p=tinymce.util.Tools.resolve("tinymce.Env"),x=tinymce.util.Tools.resolve("tinymce.util.Tools"),P=Array.prototype.push,T=/^\.(?:ephox|tiny-pageembed|mce)(?:[.-]+\w+)+$/;e.add("importcss",function(t){return r(t),e=t,{convertSelectorToFormat:function(t){return h(e,t)}};var e})}();
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";function l(e){return e.getParam("insertdatetime_timeformat",e.translate("%H:%M:%S"))}function s(e){return e.getParam("insertdatetime_formats",["%H:%M:%S","%Y-%m-%d","%I:%M:%S %p","%D"])}function r(e,t){if((e=""+e).length<t)for(var n=0;n<t-e.length;n++)e="0"+e;return e}function d(e,t,n){return void 0===n&&(n=new Date),(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=t.replace("%D","%m/%d/%Y")).replace("%r","%I:%M:%S %p")).replace("%Y",""+n.getFullYear())).replace("%y",""+n.getYear())).replace("%m",r(n.getMonth()+1,2))).replace("%d",r(n.getDate(),2))).replace("%H",""+r(n.getHours(),2))).replace("%M",""+r(n.getMinutes(),2))).replace("%S",""+r(n.getSeconds(),2))).replace("%I",""+((n.getHours()+11)%12+1))).replace("%p",n.getHours()<12?"AM":"PM")).replace("%B",""+e.translate(u[n.getMonth()]))).replace("%b",""+e.translate(o[n.getMonth()]))).replace("%A",""+e.translate(i[n.getDay()]))).replace("%a",""+e.translate(a[n.getDay()]))).replace("%%","%")}function p(e,t){var n,r,a,i,o,u;e.getParam("insertdatetime_element",!1)?(n=d(e,t),r=/%[HMSIp]/.test(t)?d(e,"%Y-%m-%dT%H:%M"):d(e,"%Y-%m-%d"),(a=e.dom.getParent(e.selection.getStart(),"time"))?(o=a,u=(i=e).dom.create("time",{datetime:r},n),o.parentNode.insertBefore(u,o),i.dom.remove(o),i.selection.select(u,!0),i.selection.collapse(!1)):e.insertContent('<time datetime="'+r+'">'+n+"</time>")):e.insertContent(d(e,t))}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),a="Sun Mon Tue Wed Thu Fri Sat Sun".split(" "),i="Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday".split(" "),o="Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),u="January February March April May June July August September October November December".split(" "),g=tinymce.util.Tools.resolve("tinymce.util.Tools");e.add("insertdatetime",function(e){var n,r,t,a,i,o,u,c;function m(e){return r.execCommand("mceInsertDate",!1,e)}(n=e).addCommand("mceInsertDate",function(e,t){p(n,null!=t?t:n.getParam("insertdatetime_dateformat",n.translate("%Y-%m-%d")))}),n.addCommand("mceInsertTime",function(e,t){p(n,null!=t?t:l(n))}),u=s(r=e),t=0<(o=s(i=r)).length?o[0]:l(i),a=t,c={get:function(){return a},set:function(e){a=e}},r.ui.registry.addSplitButton("insertdatetime",{icon:"insert-time",tooltip:"Insert date/time",select:function(e){return e===c.get()},fetch:function(e){e(g.map(u,function(e){return{type:"choiceitem",text:d(r,e),value:e}}))},onAction:function(e){m(c.get())},onItemAction:function(e,t){c.set(t),m(t)}}),r.ui.registry.addNestedMenuItem("insertdatetime",{icon:"insert-time",text:"Date/time",getSubmenuItems:function(){return g.map(u,function(e){return{type:"menuitem",text:d(r,e),onAction:(t=e,function(){c.set(t),m(t)})};var t})}})})}();
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),l=tinymce.util.Tools.resolve("tinymce.util.Tools");e.add("legacyoutput",function(e){var s,t;(t=s=e).settings.inline_styles=!1,t.getParam("fontsize_formats")||(t.settings.fontsize_formats="8pt=1 10pt=2 12pt=3 14pt=4 18pt=5 24pt=6 36pt=7"),t.getParam("font_formats")||(t.settings.font_formats="Andale Mono=andale mono,monospace;Arial=arial,helvetica,sans-serif;Arial Black=arial black,sans-serif;Book Antiqua=book antiqua,palatino,serif;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier,monospace;Georgia=georgia,palatino,serif;Helvetica=helvetica,arial,sans-serif;Impact=impact,sans-serif;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco,monospace;Times New Roman=times new roman,times,serif;Trebuchet MS=trebuchet ms,geneva,sans-serif;Verdana=verdana,geneva,sans-serif;Webdings=webdings;Wingdings=wingdings,zapf dingbats"),s.on("PreInit",function(){var e=s,t="p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table",i=l.explode(e.getParam("font_size_style_values","xx-small,x-small,small,medium,large,x-large,xx-large")),a=e.schema;e.formatter.register({alignleft:{selector:t,attributes:{align:"left"}},aligncenter:{selector:t,attributes:{align:"center"}},alignright:{selector:t,attributes:{align:"right"}},alignjustify:{selector:t,attributes:{align:"justify"}},bold:[{inline:"b",remove:"all",preserve_attributes:["class","style"]},{inline:"strong",remove:"all",preserve_attributes:["class","style"]},{inline:"span",styles:{fontWeight:"bold"}}],italic:[{inline:"i",remove:"all",preserve_attributes:["class","style"]},{inline:"em",remove:"all",preserve_attributes:["class","style"]},{inline:"span",styles:{fontStyle:"italic"}}],underline:[{inline:"u",remove:"all",preserve_attributes:["class","style"]},{inline:"span",styles:{textDecoration:"underline"},exact:!0}],strikethrough:[{inline:"strike",remove:"all",preserve_attributes:["class","style"]},{inline:"span",styles:{textDecoration:"line-through"},exact:!0}],fontname:{inline:"font",toggle:!1,attributes:{face:"%value"}},fontsize:{inline:"font",toggle:!1,attributes:{size:function(e){return String(l.inArray(i,e.value)+1)}}},forecolor:{inline:"font",attributes:{color:"%value"},links:!0,remove_similar:!0,clear_child_styles:!0},hilitecolor:{inline:"font",styles:{backgroundColor:"%value"},links:!0,remove_similar:!0,clear_child_styles:!0}}),l.each("b,i,u,strike".split(","),function(e){a.addValidElements(e+"[*]")}),a.getElementRule("font")||a.addValidElements("font[face|size|color|style]"),l.each(t.split(","),function(e){var t=a.getElementRule(e);t&&(t.attributes.align||(t.attributes.align={},t.attributesOrder.push("align")))})})})}();
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";function o(n,e){for(var a="",o=0;o<e;o++)a+=n;return a}function s(n,e){var a=n.getParam("nonbreaking_wrap",!0,"boolean")||n.plugins.visualchars?'<span class="'+(n.plugins.visualchars&&n.plugins.visualchars.isEnabled()?"mce-nbsp-wrap mce-nbsp":"mce-nbsp-wrap")+'" contenteditable="false">'+o("&nbsp;",e)+"</span>":o("&nbsp;",e);n.undoManager.transact(function(){return n.insertContent(a)})}var n=tinymce.util.Tools.resolve("tinymce.PluginManager"),c=tinymce.util.Tools.resolve("tinymce.util.VK");n.add("nonbreaking",function(n){var e,a,o,t,i;function r(){return a.execCommand("mceNonBreaking")}(e=n).addCommand("mceNonBreaking",function(){s(e,1)}),(a=n).ui.registry.addButton("nonbreaking",{icon:"non-breaking",tooltip:"Nonbreaking space",onAction:r}),a.ui.registry.addMenuItem("nonbreaking",{icon:"non-breaking",text:"Nonbreaking space",onAction:r}),0<(i="boolean"==typeof(t=(o=n).getParam("nonbreaking_force_tab",0))?!0===t?3:0:t)&&o.on("keydown",function(n){n.keyCode!==c.TAB||n.isDefaultPrevented()||n.shiftKey||(n.preventDefault(),n.stopImmediatePropagation(),s(o,i))})})}();
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";function l(t){return t.getParam("noneditable_noneditable_class","mceNonEditable")}function u(e){return function(t){return-1!==(" "+t.attr("class")+" ").indexOf(e)}}function e(e){var t,r="contenteditable",n=" "+f.trim(e.getParam("noneditable_editable_class","mceEditable"))+" ",a=" "+f.trim(l(e))+" ",i=u(n),o=u(a),c=(t=e.getParam("noneditable_regexp",[]))&&t.constructor===RegExp?[t]:t;e.on("PreInit",function(){0<c.length&&e.on("BeforeSetContent",function(t){!function(t,e,n){var r=e.length,a=n.content;if("raw"!==n.format){for(;r--;)a=a.replace(e[r],function(i,o,c){return function(t){var e=arguments,n=e[e.length-2],r=0<n?o.charAt(n-1):"";if('"'===r)return t;if(">"===r){var a=o.lastIndexOf("<",n);if(-1!==a&&-1!==o.substring(a,n).indexOf('contenteditable="false"'))return t}return'<span class="'+c+'" data-mce-content="'+i.dom.encode(e[0])+'">'+i.dom.encode("string"==typeof e[1]?e[1]:e[0])+"</span>"}}(t,a,l(t)));n.content=a}}(e,c,t)}),e.parser.addAttributeFilter("class",function(t){for(var e,n=t.length;n--;)e=t[n],i(e)?e.attr(r,"true"):o(e)&&e.attr(r,"false")}),e.serializer.addAttributeFilter(r,function(t){for(var e,n=t.length;n--;)e=t[n],(i(e)||o(e))&&(0<c.length&&e.attr("data-mce-content")?(e.name="#text",e.type=3,e.raw=!0,e.value=e.attr("data-mce-content")):e.attr(r,null))})})}var t=tinymce.util.Tools.resolve("tinymce.PluginManager"),f=tinymce.util.Tools.resolve("tinymce.util.Tools");t.add("noneditable",function(t){e(t)})}();
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";function u(e){return e.getParam("pagebreak_split_block",!1)}function l(e){var a='<img src="'+n.transparentSrc+'" class="'+m+'" data-mce-resize="false" data-mce-placeholder />';return e?"<p>"+a+"</p>":a}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),n=tinymce.util.Tools.resolve("tinymce.Env"),m="mce-pagebreak";e.add("pagebreak",function(e){var a,n,o,i,t,r;function c(){return n.execCommand("mcePageBreak")}function g(){return u(o)}(a=e).addCommand("mcePageBreak",function(){a.insertContent(l(u(a)))}),(n=e).ui.registry.addButton("pagebreak",{icon:"page-break",tooltip:"Page break",onAction:c}),n.ui.registry.addMenuItem("pagebreak",{text:"Page break",icon:"page-break",onAction:c}),i=(o=e).getParam("pagebreak_separator","\x3c!-- pagebreak --\x3e"),t=new RegExp(i.replace(/[\?\.\*\[\]\(\)\{\}\+\^\$\:]/g,function(e){return"\\"+e}),"gi"),o.on("BeforeSetContent",function(e){e.content=e.content.replace(t,l(g()))}),o.on("PreInit",function(){o.serializer.addNodeFilter("img",function(e){for(var a,n,t,r=e.length;r--;)(t=(n=e[r]).attr("class"))&&-1!==t.indexOf(m)&&(a=n.parent,o.schema.getBlockElements()[a.name]&&g()?(a.type=3,a.value=i,a.raw=!0,n.remove()):(n.type=3,n.value=i,n.raw=!0))})}),(r=e).on("ResolveName",function(e){"IMG"===e.target.nodeName&&r.dom.hasClass(e.target,m)&&(e.name="pagebreak")})})}();
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),f=tinymce.util.Tools.resolve("tinymce.Env"),w=tinymce.util.Tools.resolve("tinymce.util.Tools");e.add("preview",function(e){var n,t;function i(){return t.execCommand("mcePreview")}(n=e).addCommand("mcePreview",function(){var e,t;t=function(t){var n="",i=t.dom.encode,e=t.getParam("content_style","","string");n+='<base href="'+i(t.documentBaseURI.getURI())+'">';var o=t.getParam("content_css_cors",!1,"boolean")?' crossorigin="anonymous"':"";w.each(t.contentCSS,function(e){n+='<link type="text/css" rel="stylesheet" href="'+i(t.documentBaseURI.toAbsolute(e))+'"'+o+">"}),e&&(n+='<style type="text/css">'+e+"</style>");var a,r,s,c,d,l,m,y=-1===(c=(a=t).getParam("body_id","tinymce","string")).indexOf("=")?c:(s=(r=a).getParam("body_id","","hash"))[r.id]||s,u=-1===(m=(d=t).getParam("body_class","","string")).indexOf("=")?m:(l=d).getParam("body_class","","hash")[l.id]||"",v='<script>document.addEventListener && document.addEventListener("click", function(e) {for (var elm = e.target; elm; elm = elm.parentNode) {if (elm.nodeName === "A" && !('+(f.mac?"e.metaKey":"e.ctrlKey && !e.altKey")+")) {e.preventDefault();}}}, false);<\/script> ",g=t.getBody().dir,p=g?' dir="'+i(g)+'"':"";return"<!DOCTYPE html><html><head>"+n+'</head><body id="'+i(y)+'" class="mce-content-body '+i(u)+'"'+p+">"+t.getContent()+v+"</body></html>"}(e=n),e.windowManager.open({title:"Preview",size:"large",body:{type:"panel",items:[{name:"preview",type:"iframe",sandboxed:!0}]},buttons:[{type:"cancel",name:"close",text:"Close",primary:!0}],initialData:{preview:t}}).focus("close")}),(t=e).ui.registry.addButton("preview",{icon:"preview",tooltip:"Preview",onAction:i}),t.ui.registry.addMenuItem("preview",{icon:"preview",text:"Preview",onAction:i})})}();
+9
View File
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";var n=tinymce.util.Tools.resolve("tinymce.PluginManager"),r=tinymce.util.Tools.resolve("tinymce.Env");n.add("print",function(n){var t,i;function e(){return i.execCommand("mcePrint")}(t=n).addCommand("mcePrint",function(){r.browser.isIE()?t.getDoc().execCommand("print",!1,null):t.getWin().print()}),(i=n).ui.registry.addButton("print",{icon:"print",tooltip:"Print",onAction:e}),i.ui.registry.addMenuItem("print",{text:"Print...",icon:"print",onAction:e}),n.addShortcut("Meta+P","","mcePrint")})}();
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";function o(e){return e.getParam("save_enablewhendirty",!0)}function a(e,n){e.notificationManager.open({text:n,type:"error"})}function t(t){t.addCommand("mceSave",function(){!function(e){var n=c.DOM.getParent(e.id,"form");if(!o(e)||e.isDirty()){if(e.save(),e.getParam("save_onsavecallback"))return e.execCallback("save_onsavecallback",e),e.nodeChanged();n?(e.setDirty(!1),n.onsubmit&&!n.onsubmit()||("function"==typeof n.submit?n.submit():a(e,"Error: Form submit field collision.")),e.nodeChanged()):a(e,"Error: No form element found.")}}(t)}),t.addCommand("mceCancel",function(){var e=t,n=r.trim(e.startContent);e.getParam("save_oncancelcallback")?e.execCallback("save_oncancelcallback",e):e.resetContent(n)})}function i(t){return function(e){function n(){e.setDisabled(o(t)&&!t.isDirty())}return n(),t.on("NodeChange dirty",n),function(){return t.off("NodeChange dirty",n)}}}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),c=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),r=tinymce.util.Tools.resolve("tinymce.util.Tools");e.add("save",function(e){var n;(n=e).ui.registry.addButton("save",{icon:"save",tooltip:"Save",disabled:!0,onAction:function(){return n.execCommand("mceSave")},onSetup:i(n)}),n.ui.registry.addButton("cancel",{icon:"cancel",tooltip:"Cancel",disabled:!0,onAction:function(){return n.execCommand("mceCancel")},onSetup:i(n)}),n.addShortcut("Meta+S","","mceSave"),t(e)})}();
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";function n(e){e.keyCode!==y.TAB||e.ctrlKey||e.altKey||e.metaKey||e.preventDefault()}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),s=tinymce.util.Tools.resolve("tinymce.EditorManager"),o=tinymce.util.Tools.resolve("tinymce.Env"),a=tinymce.util.Tools.resolve("tinymce.util.Delay"),c=tinymce.util.Tools.resolve("tinymce.util.Tools"),y=tinymce.util.Tools.resolve("tinymce.util.VK"),d=t.DOM;e.add("tabfocus",function(e){function t(l){var r,e,t,n,i;l.keyCode!==y.TAB||l.ctrlKey||l.altKey||l.metaKey||l.isDefaultPrevented()||(e=function(e){function t(e){return/INPUT|TEXTAREA|BUTTON/.test(e.tagName)&&s.get(l.id)&&-1!==e.tabIndex&&i(e)}var n=d.select(":input:enabled,*[tabindex]:not(iframe)"),i=function(e){return"BODY"===e.nodeName||"hidden"!==e.type&&"none"!==e.style.display&&"hidden"!==e.style.visibility&&i(e.parentNode)};if(c.each(n,function(e,t){if(e.id===u.id)return r=t,!1}),0<e){for(var o=r+1;o<n.length;o++)if(t(n[o]))return n[o]}else for(o=r-1;0<=o;o--)if(t(n[o]))return n[o];return null},1===(t=c.explode(u.getParam("tab_focus",u.getParam("tabfocus_elements",":prev,:next")))).length&&(t[1]=t[0],t[0]=":prev"),(n=l.shiftKey?":prev"===t[0]?e(-1):d.get(t[0]):":next"===t[1]?e(1):d.get(t[1]))&&(i=s.get(n.id||n.name),n.id&&i?i.focus():a.setTimeout(function(){o.webkit||window.focus(),n.focus()},10),l.preventDefault()))}var u;(u=e).on("init",function(){u.inline&&d.setAttrib(u.getBody(),"tabIndex",null),u.on("keyup",n),o.gecko?u.on("keypress keydown",t):u.on("keydown",t)})})}();
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("textcolor",function(){})}();
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";function a(t){return t.getParam("toc_class","mce-toc")}function s(t){var e=t.getParam("toc_header","h2");return/^h[1-6]$/.test(e)?e:"h2"}function u(n){return function(t){function e(){return t.setDisabled(n.mode.isReadOnly()||!(0<g(n).length))}return e(),n.on("LoadContent SetContent change",e),function(){return n.on("LoadContent SetContent change",e)}}}function d(t){var e,n,o,i="",r=g(t),c=function(t){for(var e=9,n=0;n<t.length;n++)if(1===(e=t[n].level<e?t[n].level:e))return e;return e}(r)-1;if(!r.length)return"";i+=(e=s(t),n=v.translate("Table of Contents"),o="</"+e+">","<"+e+' contenteditable="true">'+m.DOM.encode(n)+o);for(var l=0;l<r.length;l++){var a=r[l];a.element.id=a.id;var u=r[l+1]&&r[l+1].level;if(c===a.level)i+="<li>";else for(var d=c;d<a.level;d++)i+="<ul><li>";if(i+='<a href="#'+a.id+'">'+a.title+"</a>",u!==a.level&&u)for(d=a.level;u<d;d--)i+=d===u+1?"</li></ul><li>":"</li></ul>";else i+="</li>",u||(i+="</ul>");c=a.level}return i}function f(t){var e=a(t),n=t.$("."+e);n.length&&t.undoManager.transact(function(){n.html(d(t))})}var t,e=tinymce.util.Tools.resolve("tinymce.PluginManager"),m=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),v=tinymce.util.Tools.resolve("tinymce.util.I18n"),c=tinymce.util.Tools.resolve("tinymce.util.Tools"),l=(t=0,function(){return"mcetoc_"+(new Date).getTime().toString(32)+(t++).toString(32)}),g=function(n){var t,o=a(n),e=s(n),i=function(t){for(var e=[],n=1;n<=t;n++)e.push("h"+n);return e.join(",")}(1<=(t=parseInt(n.getParam("toc_depth","3"),10))&&t<=9?t:3),r=n.$(i);return r.length&&/^h[1-9]$/i.test(e)&&(r=r.filter(function(t,e){return!n.dom.hasClass(e.parentNode,o)})),c.map(r,function(t){return{id:t.id||l(),level:parseInt(t.nodeName.replace(/^H/i,""),10),title:n.$.text(t),element:t}})};e.add("toc",function(t){var c,e,n,o,i,r;function l(){return i.execCommand("mceInsertToc")}(c=t).addCommand("mceInsertToc",function(){var t,e,n,o,i,r;o=a(t=c),i=t.$("."+o),r=t,!i.length||0<r.dom.getParents(i[0],".mce-offscreen-selection").length?t.insertContent((n=d(e=t),'<div class="'+e.dom.encode(a(e))+'" contenteditable="false">'+n+"</div>")):f(t)}),c.addCommand("mceUpdateToc",function(){f(c)}),(i=t).ui.registry.addButton("toc",{icon:"toc",tooltip:"Table of contents",onAction:l,onSetup:u(i)}),i.ui.registry.addButton("tocupdate",{icon:"reload",tooltip:"Update",onAction:function(){return i.execCommand("mceUpdateToc")}}),i.ui.registry.addMenuItem("toc",{icon:"toc",text:"Table of contents",onAction:l,onSetup:u(i)}),i.ui.registry.addContextToolbar("toc",{items:"tocupdate",predicate:(r=i,function(t){return t&&r.dom.is(t,"."+a(r))&&r.getBody().contains(t)}),scope:"node",position:"node"}),n=(e=t).$,o=a(e),e.on("PreProcess",function(t){var e=n("."+o,t.node);e.length&&(e.removeAttr("contentEditable"),e.find("[contenteditable]").removeAttr("contentEditable"))}),e.on("SetContent",function(){var t=n("."+o);t.length&&(t.attr("contentEditable",!1),t.children(":first-child").attr("contentEditable",!0))})})}();
@@ -0,0 +1,9 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*
* Version: 5.10.9 (2023-11-15)
*/
!function(){"use strict";function f(t,o,e){var n,i;t.dom.toggleClass(t.getBody(),"mce-visualblocks"),e.set(!e.get()),n=t,i=e.get(),n.fire("VisualBlocks",{state:i})}function g(e,n){return function(o){function t(t){return o.setActive(t.state)}return o.setActive(n.get()),e.on("VisualBlocks",t),function(){return e.off("VisualBlocks",t)}}}tinymce.util.Tools.resolve("tinymce.PluginManager").add("visualblocks",function(t,o){var e,n,i,s,c,u,l,a=(e=!1,{get:function(){return e},set:function(t){e=t}});function r(){return s.execCommand("mceVisualBlocks")}i=a,(n=t).addCommand("mceVisualBlocks",function(){f(n,0,i)}),(s=t).ui.registry.addToggleButton("visualblocks",{icon:"visualblocks",tooltip:"Show blocks",onAction:r,onSetup:g(s,c=a)}),s.ui.registry.addToggleMenuItem("visualblocks",{text:"Show blocks",icon:"visualblocks",onAction:r,onSetup:g(s,c)}),l=a,(u=t).on("PreviewFormats AfterPreviewFormats",function(t){l.get()&&u.dom.toggleClass(u.getBody(),"mce-visualblocks","afterpreviewformats"===t.type)}),u.on("init",function(){u.getParam("visualblocks_default_state",!1,"boolean")&&f(u,0,l)})})}();
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
body{background-color:#2f3742;color:#dfe0e4;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}a{color:#4099ff}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#6d737b}figure{display:table;margin:1rem auto}figure figcaption{color:#8a8f97;display:block;margin-top:.25rem;text-align:center}hr{border-color:#6d737b;border-style:solid;border-width:1px 0 0 0}code{background-color:#6d737b;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #6d737b;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #6d737b;margin-right:1.5rem;padding-right:1rem}
@@ -0,0 +1,7 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem}
@@ -0,0 +1,7 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
@media screen{html{background:#f4f4f4;min-height:100%}}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif}@media screen{body{background-color:#fff;box-shadow:0 0 4px rgba(0,0,0,.15);box-sizing:border-box;margin:1rem auto 0;max-width:820px;min-height:calc(100vh - 1rem);padding:4rem 6rem 6rem 6rem}}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#ccc}figure figcaption{color:#999;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem}
@@ -0,0 +1,7 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem auto;max-width:900px}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem}
File diff suppressed because one or more lines are too long

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