# 휴가·출장·교육 조회 API — 별도 구현(2026-08-11, 근무일정 제외·교육 추가 2026-08-12)

**이 폴더의 것은 제품 소스에 반영하지 않았고, 개발서버에도 올리지 않았다.**
`~/5240lab/kiwibox_eGov4.2` 아래는 한 줄도 건드리지 않았다. 무엇을 만들면 되는지 보이기 위한 산출물이다.

## 왜 새로 만드는가

제품의 JSON API(`SYS/API/json`) 컨트롤러 URL 24개를 전수 확인했다. 근태 쪽은 근태기 적재와
PC-OFF 처리뿐이고, 조회 계열은 조직·직원·SSO·발령·결재다. **휴가·출장·교육을 기간으로 뽑는
엔드포인트는 없다.**

가장 가까운 결재 3종(`getApprovalData` / `getApprovalApplData` / `getApprovalReqData`)도 못 쓴다.
SQL을 직접 읽고 확인한 이유다.

- `APPL_STAFF_ID = 기안자` 또는 결재자 조건이 박혀 있어 **개인 단위**다. 조직 단위 조회가 안 된다.
- 기간 필터가 `APPL_YMD`(**기안일**)다. 캘린더는 휴가 기간으로 뽑아야 하는데 그 조건이 없다.
- 휴가 시작·종료일과 휴가코드가 컬럼이 아니라 `EAPF_REQST_MOBILE()`이 렌더링한 **문서 본문(CLOB)**
  안에 있다. 파싱해서 쓸 물건이 아니다.

## 만든 것

| 파일 | 무엇 |
|---|---|
| `TaaCalendarApi_SQL.xml` | 신규 매퍼 SQL 3개 — 권한 확인, 일정 조회(휴가·출장·교육), 호출 기록 |
| `TaaCalendarController.java` | `POST /getTaaCalendarApiList.do` |
| `TaaCalendarServiceImpl.java` | 입력 확인 → 키 확인 → 조회 → 기록 |
| `TaaCalendarMapper.java` | 매퍼 인터페이스 |
| `sample-request.json` / `sample-response.json` | 요청·응답 형태 |
| `option-TAA_CAL_INF_YN.md` | 가정한 신규 옵션의 정의 |

엔드포인트는 **하나**로 묶었다. 갈래를 따로 두면 호출도 그만큼, 키 확인도 그만큼이고 기간·조직
조건을 여러 곳에 똑같이 유지해야 한다. `kindCds`로 고르게 했다.

## 판단한 것들

**조회 범위는 연동 계정의 조직조회권한이 정한다.**
`AUTT_SRCH_BASE`(AUTH_TYPE_CD='111') + `ORGF_LINE` — 제품 홈 화면(`Home_SQL.xml` L744-L761)이 쓰는
바로 그 패턴을 옮겼다. 요청의 `orgCd`는 그 안에서 **더 좁히기만** 한다. 키가 새더라도 전사
인사정보가 통째로 나가지 않게 하려는 것이다.

**조직은 하위조직을 포함하지 않는다.** ← 구현 전에 반드시 읽을 것
부서를 고르면 **그 부서에 직접 속한 직원만** 나간다. 하위 부서 직원은 나가지 않는다. 그래서
`inclOrgYn` 파라미터를 아예 없앴다 — 받아 두면 언젠가 `'Y'`가 들어와 조용히 범위가 넓어진다.
조직 필터는 `K.ORG_CD = orgCd` 한 줄이다.

> **그렇다고 `ORGF_LINE`을 빼면 안 된다.** `orgScope`의 `ORGF_LINE`/`ORG_LINE LIKE`는
> "요청 조직의 하위"가 아니라 **"연동 계정에게 권한이 있는 조직의 하위"**를 계산하는 자리다.
> 이건 권한 경계라, 없애면 권한 밖 조직의 인사정보가 새어 나간다. 하위조직 불포함과는
> 다른 이야기다. 아래 성능 항목도 이 때문에 그대로 남아 있다.

**근무일정은 연동하지 않는다.**(2026-08-12 결정)
매일 값이 있어(기본근무·유일·휴무 포함) 직원 수 × 365일이 그대로 캘린더에 쌓인다. 휴가가 그
속에 묻혀 캘린더가 못 쓰게 된다 — **연동량 과다**가 이유이지 구현 난도가 아니다. `WKTYPE`
분기를 SQL·서비스·샘플요청에서 모두 뺐다.

> 되살릴 일이 생기면 **UNION 하나를 더하는 일이 아니다.** 근무일정조회 화면은 신청 테이블의
> 합집합이 아니라 일자별로 우선순위를 적용해 **해결한 결과 1건**을 보여준다 — 조직 근무계획과
> 개인 신청이 겹치면 개인 신청이 우선하고, 변경은 변경일자 이후만 반영된다(근태 매뉴얼
> 「근무일정조회/근무일정표/근무시간표」). 신청 테이블을 그대로 합치면 화면에는 1건인 날이
> 캘린더에는 2건으로 뜬다. 그 우선순위 규칙을 SQL에 재구현하면 제품과 갈린다 — 옵션 1426을
> 연동 쪽에 복사하지 않기로 한 것과 같은 이유다.

**교육은 신청 마스터가 둘이다 — 계획은 뺐다.**(2026-08-12)
「무엇을 실을지부터 정해야 한다」로 남아 있던 항목이다. 리버스 문서(`spec-db/Table/*`)로 확인해
다음과 같이 정했다.

| 테이블 | 무엇 | 캘린더 | 왜 |
|---|---|---|---|
| `EDUT_REQ` | 개설된 차수에 신청 | **싣는다** | 기간이 이 테이블에 없다 — `EDUT_COURSN`(과정·시작일·차수)에서 종료일을, `EDUT_COURS`에서 과정명을 가져온다 |
| `EDUT_HST2` | 과정명을 직접 적어 신청(외부 교육) | **싣는다** | `STA_YMD`·`END_YMD`가 자체에 있어 그대로 쓴다 |
| `EDUT_PLAN_REQ` / `PLAN_REQ2` | 교육계획신청(수요조사) | **뺀다** | 수강예정 **년월(`EDU_YM`)만** 있고 날짜가 없다. 확정 일정이 아니라 수요조사다 |

데모 서비스영역 건수: `EDUT_REQ` 2, `EDUT_HST2` 10, `EDUT_PLAN_REQ` 3, `EDUT_COURSN` 149.

교육 과정명은 공통코드가 아니라 자유 문자열이라 `CMMF_CODE_ML`로 못 뽑는다. 그래서 갈래마다
`TYPE_NM_RAW`에 이름을 담아 올리고, 바깥에서 `NVL(TYPE_NM_RAW, CMMF_CODE_ML(...))`로 받는다 —
휴가·출장은 지금까지처럼 공통코드를 쓰고 교육만 자기 이름을 쓴다.

> `EDUT_REQ`·`EDUT_HST2`에는 **최종수정일시 컬럼이 없다.** `CHG_DATE`를 NULL로 내보내므로
> 증분 비교가 교육에는 듣지 않는다. 대조로만 맞춰야 한다.

**취소는 "응답에서 사라짐"으로 나타난다.**
5240에는 삭제도 변경도 없다. **취소만 있고, 취소한 뒤에는 새로 결재를 받는다** — 원래 건을
끝내고 새 신청건이 생기는 구조다. 취소에는 **휴가취소 결재 프로세스가 따로 있어서, 취소승인이
나야 결재완료됐던 원래 건이 원복된다.** 그리고 **캘린더에 실리는 것은 결재완료(30)가 기본이고,
결재중(20)은 콘솔에서 「결재중도 포함」을 켰을 때만 더해진다.** 임시저장(10)·반려(40)는 어느
경우에도 나가지 않고, 원복된 건도 이 필터에서 빠진다.

이 구조가 연동을 단순하게 만든다.

- 신청건 하나가 캘린더 일정 하나에 그대로 대응한다. 이미 나간 일정의 날짜나 휴가종류가 뒤에서
  바뀌는 일이 없다 — 고치려면 취소하고 새로 신청하므로, 그것은 다른 신청번호의 새 일정이다.
- 바뀌는 것은 결재중(20) → 결재완료(30) 전이 하나뿐이다. 이때 제목의 `[진행중]`만 떼면 된다.
- 취소는 다음 조회에서 그 건이 빠지는 것으로 드러난다. 연동 쪽은 기간 창 전체를 다시 받아
  캘린더와 대조하고, 없어진 것을 지운다. 사라지는 시점은 취소를 낸 때가 아니라 **취소승인이
  난 때**다 — 그 사이에는 원래 일정이 캘린더에 남아 있다.
- 옵션 1426이 바뀌어 어떤 휴가종류가 미표시로 돌아서는 경우도 같은 방식으로 정리된다.
  그 건이 응답에서 빠지기 때문이다. 따로 찾아 지우는 코드가 필요 없다.

> **확인 완료(2026-08-12, 데이터 대조).** ⑴ 취소는 `EAPT_CANCL_REQ`(결재취소 신청서)에 따로
> 쌓인다 — 휴가 테이블에는 취소신청이 늘지 않으므로 "취소했더니 일정이 하나 더" 문제는 없다.
> ⑵ 취소승인이 나면 원건 `REQ_STATUS_CD`가 **40(반려)** 으로 바뀐다(취소신청서 1578→원건 1572,
> 101255→101254 모두 40). 상태 필터만으로 빠지므로 SQL에 조건을 더할 필요가 없다. 취소신청이
> 결재중인 건의 원건은 30 그대로였다(101246→101022) — 취소는 승인된 것만 반영한다는 규칙과 맞다.

> **이 테이블에는 휴가신청만 있는 것이 아니다.** `EAPT_RULE` 기준 `TAAT_LEAV_REQ`를 쓰는 신청서는
> 셋이다 — 휴가신청(1022)·조퇴/외출(1122)·경조휴가(1123). **셋 다 내보낸다**(2026-08-12 결정).
> 조퇴/외출도 시스템 구조상 휴가이고, 무엇을 감출지는 오로지 옵션 1426이 정한다. 그래서 신청서
> 종류로 거르는 조건도, `EAPT_REQ` 조인도 두지 않는다 — 통째로 읽는 지금 구조가 맞다.

> 대조할 때의 규칙은 준비사항 `change` 항목에 적었다. 가장 중요한 것 — **정상 응답으로 0건이
> 올 수 있다.** 연동 계정의 조직조회권한이 좁아지거나 조직이 개편되면 오류 없이 빈 결과가 온다.
> 그대로 대조하면 전 직원 일정이 한꺼번에 지워진다. 삭제 쪽에 안전장치가 필요하다.

**옵션 1426(HIDE_LEAV_CDS)은 API에서 적용한다.**
미표시 코드(`CNFG_VAL01`)는 응답에 아예 넣지 않고, 마스킹 코드(`CNFG_VAL02`)는 `MASK_YN='Y'`로
표시해 내보낸다. 연동 쪽에 규칙을 복사해 두면 제품과 기준이 갈린다.

**마스킹 표시명(`CNFG_VAL03`)도 응답에 싣는다(`MASK_NM`).** 연동은 API로 옮기고 나면 `CFGT_VAL`을
직접 읽지 못한다. `MASK_YN='Y'`만 받고 무엇으로 바꿔 쓸지 모르면 마스킹이 반쪽이 된다 — 지금
콘솔은 DB를 직접 읽어 이 값을 얻고 있어서 드러나지 않던 구멍이다. 값이 비어 있을 때 무엇으로
내보낼지는 연동이 정해야 한다(데모 서비스영역은 지금 비어 있다).

**기간은 겹치면 뽑는다.**
`STA_YMD <= endYmd AND END_YMD >= staYmd`. 지금 콘솔의 데모 조회는 `STA_YMD`만 봐서 **달을 걸친
휴가가 빠진다**. 이 API에서는 고쳤다.

**상태 기본값은 30**(결재완료)뿐이다. 결재중(20)은 연동이 **콘솔 설정(「결재중도 포함」)에 따라
`reqStatusCds`에 더해 보낼 때만** 나간다 — 기본값을 `20,30`으로 두면 연동이 값을 빠뜨렸을 때
결재도 안 끝난 휴가가 조용히 캘린더로 나간다. 임시저장(10)·반려(40)는 어느 경우에도 안 된다.

**조회 실패와 빈 결과를 구분한다.**
`retCode`가 0이 아니면 연동 쪽은 캘린더를 건드리지 않아야 한다. 빈 배열을 "휴가가 없다"로 읽으면
이미 등록한 일정을 몽땅 지운다.

**기간 상한 400일.** 넓힐수록 응답이 커지고 느려진다.

## 확인하지 못한 것

- **실행해 보지 않았다.** 컴파일도, 쿼리 실행도 하지 않았다. 개발서버 반영이 없기 때문이다.
  검증하려면 kiwibox 빌드 환경과 WAS가 필요하다.
- `TAA_BSTRIP_TYPE_CD` 코드값 목록은 spec-db에 미확인으로 남아 있다.
  `CMMF_CODE_ML`로 이름을 뽑게 해 두었으므로 코드표가 있으면 그대로 나온다.

## 이 다음

이 상태로는 제품에 넣을 수 없다. 넣으려면 브랜치·릴리스·WAS 재기동, 그리고 고객사 설치본 반영
시점이 정해져야 한다. 그 전에 SQL 실행 계획과 `ORGF_LINE` 호출 비용을 실데이터에서 재 보는 것이
먼저다 — 조직 범위 계산이 행마다 함수를 부르는 구조라 직원 수가 많으면 여기서 느려진다.
하위조직을 안 쓰기로 했다고 이 비용이 사라지지 않는다. `ORGF_LINE`은 **권한 범위**를 계산하는
쪽에 남아 있고, 줄어든 것은 요청 조직 쪽 `LIKE` 하나뿐이다.
