# 5240 ↔ 구글 캘린더 연동 — 기능정의서

**버전** 0.1 (2026-08-12) · **상태** 개발 착수용 초안
**전제** 이 문서의 규칙은 대부분 `google.insapien.co.kr` 콘솔에서 이미 화면으로 구현해 확인한 것이다.
글로만 있는 규칙과 화면으로 확인한 규칙을 섞지 않기 위해, 각 항목에 **근거**를 함께 적는다.

---

## 0. 이 연동이 하는 일

5240 근태에 쌓인 **휴가·출장·교육 일정을 구글 캘린더로 내보낸다.** 단방향이다 —
구글 캘린더의 일정을 5240으로 들여오지 않고, 직원 개인 일정을 읽지도 않는다.

받는 쪽은 회사가 만든 **공유 캘린더 하나**이고, 직원은 그것을 각자 구독한다.

### 범위 밖 (명시적으로 제외)

| 제외 대상 | 이유 |
|---|---|
| **근무일정(근무유형)** | 기본근무·유일·휴무까지 매일 값이 있어 직원 수 × 365일이 쌓인다. 휴가가 그 속에 묻혀 캘린더가 못 쓰게 된다 — **연동량 과다**이지 구현 난도가 아니다 |
| **교육계획신청**(`EDUT_PLAN_REQ`/`PLAN_REQ2`) | 수강예정 **년월만** 있고 날짜가 없다. 확정 일정이 아니라 수요조사다 |
| **구글 → 5240 역방향** | 하지 않는다 |
| **직원 개인 캘린더 직접 쓰기**(도메인 전체 위임) | 기능은 설계에 있으나 이번 범위에서 잠근다. Workspace 최고관리자 승인과 보안 검토가 선행 |

> 근무일정을 되살릴 일이 생기면 **UNION 하나를 더하는 일이 아니다.** 근무일정조회 화면은
> 신청 테이블의 합집합이 아니라 일자별 우선순위를 적용한 **결과 1건**을 보여준다(조직 근무계획과
> 개인 신청이 겹치면 개인 신청 우선, 변경은 변경일자 이후만 반영). 근거: 근태 매뉴얼
> 「근무일정조회/근무일정표/근무시간표」.

---

## 1. 용어

| 용어 | 뜻 |
|---|---|
| **연동함(tenant)** | 고객사 하나. 서비스영역 ID(`SERVAREA_ID`) 하나에 연동 설정 한 벌이 붙는다 |
| **연동 계정** | 5240에서 자료를 읽어 갈 때 쓰는 직원 계정. **이 계정의 조직조회권한이 곧 동기화 범위** |
| **갈래(branch)** | 내보낼 일정의 종류 — 휴가 / 출장 / 교육 |
| **기간 창(window)** | 내보낼 기간. 오늘 기준 과거 N개월 ~ 미래 M개월로 **매 실행 시 다시 계산** |
| **대조(reconcile)** | 기간 창 전체를 다시 읽어 캘린더와 맞추는 것. 없어진 것은 지우고 달라진 것은 고친다 |

---

## 2. 기능 목록

| # | 기능 | 주체 | 비고 |
|---|---|---|---|
| F-01 | 연동함 등록·전환 | 5240 운영 | 서비스영역 ID 단위 |
| F-02 | 구글 계정 종류 판별 및 연동 방식 선택 | 고객사 인사담당 | 콘솔 1단계 |
| F-03 | 구글 OAuth 연결(클라이언트 등록·동의) | 고객사 인사담당 | 콘솔 2단계 ① |
| F-04 | 내보낼 범위·갈래·기간·상태 설정 | 고객사 인사담당 | 콘솔 2단계 ② |
| F-05 | 내보내기 미리보기 | 고객사 인사담당 | 콘솔 3단계 |
| F-06 | 직원 안내문·공지문 생성 | 고객사 인사담당 | 콘솔 4단계 |
| F-07 | **5240 일정 조회 API** | 5240 제품 | 신규 개발 — `api-proposal/` 참조 구현 있음 |
| F-08 | **캘린더 동기화(생성·수정·삭제)** | 연동 서버 | 신규 개발 — 미구현 |
| F-09 | 동기화 결과 통지 | 연동 서버 | 실패·중단 시 담당자 메일 |
| F-10 | 준비 상태 점검(무엇이 막고 있나) | 콘솔 | 구현됨 |

F-01~F-06, F-10은 콘솔에 **이미 구현되어 동작한다.** 신규 개발은 **F-07·F-08·F-09**다.

---

## 3. 대상 데이터

### 3.1 휴가 — `TAAT_LEAV_REQ`

**이 테이블에는 휴가신청만 있는 것이 아니다.** `EAPT_RULE` 기준 이 테이블을 쓰는 신청서는 셋이다.

| APPL_CD | 신청서 | 캘린더 |
|---|---|---|
| 1022 | 휴가신청 | 내보낸다 |
| 1122 | 조퇴/외출 신청 | **내보낸다** |
| 1123 | 경조휴가신청 | **내보낸다** |

조퇴/외출도 시스템 구조상 휴가다. **무엇을 감출지는 오로지 옵션 1426이 정한다** — 신청서 종류로
거르지 않는다. 따라서 `EAPT_REQ` 조인이 필요 없고, 테이블을 통째로 읽는다.

### 3.2 출장 — `TAAT_BSTRIP_REQ`

단일 테이블. 시작·종료 일시가 모두 있다.

### 3.3 교육 — `EDUT_REQ` + `EDUT_HST2` (둘)

| 테이블 | 무엇 | 기간을 어디서 얻나 |
|---|---|---|
| `EDUT_REQ` | 개설된 차수에 신청 | 이 테이블에 **없다** → `EDUT_COURSN`(과정·시작일·차수)에서 종료일, `EDUT_COURS`에서 과정명 |
| `EDUT_HST2` | 과정명 직접입력(외부 교육) | 자체 보유(`STA_YMD`/`END_YMD`) |

교육 과정명은 **공통코드가 아니라 자유 문자열**이라 `CMMF_CODE_ML`로 뽑을 수 없다.
갈래마다 `TYPE_NM_RAW`에 이름을 담아 올리고 `NVL(TYPE_NM_RAW, CMMF_CODE_ML(...))`로 받는다.

> **교육에는 최종수정일시 컬럼이 없다.** `CHG_DATE`가 NULL이므로 **증분 비교가 교육에는 듣지 않는다.**
> 대조로만 맞춘다.

---

## 4. 업무 규칙

### R-01 · 조회 범위는 연동 계정의 조직조회권한이 정한다

`AUTT_SRCH_BASE`(AUTH_TYPE_CD='111') + `ORGF_LINE`. 제품 홈 화면과 같은 패턴이다.
화면에서 전사를 골라도 **권한 밖 직원은 나가지 않는다.** 요청의 `orgCd`는 그 안에서 **더 좁히기만** 한다.

**이유** — 인터페이스 키가 새더라도 전사 인사정보가 통째로 나가지 않게 하려는 것이다.
키는 "누구로서 조회하는가"까지만 정하고, 무엇이 보이는가는 제품의 권한 설정이 정한다.

### R-02 · 조직은 하위조직을 포함하지 않는다

부서를 고르면 **그 부서에 직접 속한 직원만** 나간다. 하위 부서는 들어가지 않는다.
`inclOrgYn` 같은 파라미터를 **두지 않는다** — 받아 두면 언젠가 `'Y'`가 들어와 범위가 조용히 넓어진다.

> ⚠ **하위 불포함이라고 `ORGF_LINE`을 빼면 안 된다.** 그 함수는 "요청 조직의 하위"가 아니라
> **"연동 계정에게 권한이 있는 조직의 하위"** 를 계산하는 **권한 경계**다. 없애면 권한 밖 인사정보가 샌다.
> 이름이 닮아 혼동하기 쉬운 자리라 못박는다.

### R-03 · 캘린더에 실리는 상태는 결재완료(30)가 기본

| 상태 | 캘린더 |
|---|---|
| 10 임시저장 | 나가지 않음 |
| 20 결재중 | **설정에서 「결재중도 포함」을 켠 경우에만.** 제목 앞에 `[진행중]` |
| 30 결재완료 | 나감 |
| 40 반려 | 나가지 않음 |

API의 `reqStatusCds` 기본값은 **`30`** 이다. 기본을 `20,30`으로 두면 연동이 값을 빠뜨렸을 때
결재도 안 끝난 휴가가 조용히 나간다 — **빠뜨린 쪽이 안전한 값**이어야 한다.

### R-04 · 취소는 「응답에서 사라짐」으로 나타난다

5240에는 **삭제도 변경도 없다. 취소만 있고, 취소 후에는 새로 결재를 받는다.** 부분 취소는 없다 —
한 신청건은 통째로 살아 있거나 통째로 없다. 그래서 **신청건 하나 = 캘린더 일정 하나**가 성립한다.

- 취소는 휴가 테이블이 아니라 **`EAPT_CANCL_REQ`(결재취소 신청서)** 에 따로 쌓인다.
  → 취소신청이 결재를 도는 동안 휴가 테이블에는 아무것도 늘지 않는다.
- **취소승인이 나면 원건 `REQ_STATUS_CD`가 40(반려)으로 바뀐다.** → R-03 필터에서 저절로 빠진다.
- **취소는 승인된 것만 반영한다.** 취소신청이 결재중인 동안 원건은 30이므로 **캘린더에 그대로 남는다.**
  별도 표시를 붙이지 않는다.

**근거(데모 데이터 대조, 2026-08-12)**

| 취소신청서 | 상태 | 원건 | 원건 상태 |
|---|---|---|---|
| 1578 | 30 승인 | 1572 | 40 |
| 101255 | 30 승인 | 101254 | 40 |
| 101246 | 20 결재중 | 101022 | 30 유지 |

> 이미 나간 일정의 날짜·종류는 **뒤에서 바뀌지 않는다.** 유일한 변화는 20 → 30 전이이고,
> 그때 제목의 `[진행중]`만 뗀다.

### R-05 · 무엇을 감출지는 옵션 1426(HIDE_LEAV_CDS)이 정한다

| 값 | 뜻 | 처리 |
|---|---|---|
| `CNFG_VAL01` | 미표시 코드 | **응답에 아예 넣지 않는다** — 연동 쪽에서 거를 기회를 주지 않는 편이 안전하다 |
| `CNFG_VAL02` | 마스킹 코드 | `MASK_YN='Y'`로 표시해 내보낸다 |
| `CNFG_VAL03` | 마스킹 표시명 | `MASK_NM`으로 함께 내보낸다 |

규칙을 연동 쪽에 복사해 두지 않는다 — 복사하면 제품과 기준이 갈린다.

- **표시명도 응답에 실어야 한다.** 연동은 API로 옮기고 나면 `CFGT_VAL`을 직접 읽지 못한다.
  `MASK_YN`만 받고 무엇으로 바꿔 쓸지 모르면 마스킹이 반쪽이 된다.
- 표시명이 비어 있을 때 무엇으로 내보낼지는 **정해야 한다**(데모 서비스영역은 현재 비어 있음).
- **출장·교육에는 가림 규칙을 두지 않는다.**

### R-06 · 기간은 겹치면 뽑는다

`STA_YMD <= endYmd AND END_YMD >= staYmd`. 시작일만 보면 **달을 걸친 휴가가 빠진다.**

기간 창은 **오늘 기준으로 매 실행 시 다시 계산**된다(고정 날짜가 아니다). 상한 400일.

### R-07 · 조회 실패와 빈 결과를 구분한다

`retCode`가 0이 아니면 연동은 **캘린더를 건드리지 않는다.** 빈 배열을 "휴가가 없다"로 읽으면
이미 등록한 일정을 몽땅 지운다.

---

## 5. 동기화 규칙 (F-08)

취소·미표시 전환이 모두 "응답에서 사라짐"으로 나타나므로, 동기화는 **기간 창 스냅샷 대조**다.
훅도 변경분 조회도 필요 없다.

### 대조 절차

1. 기간 창 전체를 API로 읽는다
2. 캘린더에서 **이 연동이 만든 일정**만 골라 온다
3. 응답에 있고 캘린더에 없다 → **생성**
4. 양쪽에 있고 내용이 다르다 → **수정** (사실상 20→30 전이뿐)
5. 캘린더에 있고 응답에 없다 → **삭제**

### 안전장치 (필수)

| # | 규칙 | 이유 |
|---|---|---|
| S-01 | **이 연동이 만든 일정만 지운다.** 이벤트마다 표식(신청번호·갈래·직원ID)을 남긴다 | 사람이 손으로 넣은 일정을 지우면 안 된다 |
| S-02 | 조회 실패 시 **아무것도 지우지 않는다** | R-07 |
| S-03 | **정상 응답 0건·급감도 의심한다.** 삭제 예정 건수가 임계를 넘으면 중단하고 담당자에게 알린다 | 권한 축소·조직 개편이면 오류 없이 0건이 온다 → **전 직원 일정 일괄 삭제 사고** |
| S-04 | **기간 창 안만 대조한다** | 창이 하루씩 움직이므로, 창 밖까지 대조하면 멀쩡한 과거 일정을 "응답에 없다"고 지운다 |
| S-05 | 삭제 후 재생성이 아니라 **수정**한다 | 직원이 걸어 둔 알림 설정이 사라지지 않는다. 단 교육은 `CHG_DATE`가 없어 증분 비교 불가 |

---

## 6. 고객사 설정 항목 (F-02~F-04)

| 항목 | 값 | 비고 |
|---|---|---|
| 계정 종류 | 워크스페이스 / 개인 지메일 | **방식보다 먼저 정한다.** 관리 콘솔 유무로 판별 |
| 연동 방식 | 공유 캘린더 / 담당자 확인용 / 도메인 위임(잠금) | 기본은 공유 캘린더 |
| 공유 방법 | 주소를 받아 초대 / 링크 공개 | **개인 지메일 조직에서만.** 유출 위험을 고객사가 고른다 |
| 범위 | 본인 / 부서 / 전사 | 하위조직 불포함(R-02) |
| 갈래 | 휴가 / 출장 / 교육 | 근무일정 없음 |
| 기간 창 | 과거 N개월 / 미래 M개월 | 오늘 기준 재계산 |
| 결재중 포함 | 예 / 아니오 | R-03 |
| 캘린더 ID | `…@group.calendar.google.com` | |

### 계정 종류가 가르는 것

| | 워크스페이스 | 개인 지메일 |
|---|---|---|
| OAuth 동의화면 | **내부 앱** — 게시·검증·테스터 한도·7일 만료 전부 없음 | 외부 앱 강제 — 프로덕션 게시 필요 |
| 캘린더 공유 대상 | 도메인·그룹에 한 번에 | 주소를 받아 초대하거나 링크 공개 |
| 직원↔구글 계정 매핑 | 회사 메일이 곧 구글 계정 | 회사가 모르는 개인 주소 — 직원에게 받아야 함 |
| 도메인 전체 위임 | 가능(잠금 상태) | **불가** |

**OAuth 클라이언트는 고객사가 소유한다**(2026-08-12 결정). 따라서 워크스페이스 고객사는
동의화면을 「내부」로 두어 게시·검증이 아예 필요 없다.

---

## 7. 인터페이스 요약

`POST /getTaaCalendarApiList.do` — 서버 간 호출. 기존 JSON API 24개와 같은 형식.

- 인증: 옵션 `TAA_CAL_INF_YN`의 사이트ID·인터페이스 키 + 연동 계정 직원번호
- 갈래는 `kindCds`로 고른다(`LEAV,BSTRIP,EDU`)
- 호출 기록은 `CMMT_MSG_LOG`에 남긴다 — "캘린더에 왜 저 일정이 있느냐"를 되짚는 근거

상세와 참조 구현(SQL·컨트롤러·서비스·요청/응답 샘플)은 **`docs/api-proposal/`** 에 있다.
**이 폴더는 제품 소스에 반영하지 않았고 실행해 본 적도 없다** — 컴파일·쿼리 실행 모두 미검증이다.

---

## 8. 열려 있는 결정

| # | 항목 | 누가 |
|---|---|---|
| O-01 | 마스킹 표시명이 비어 있을 때 캘린더에 무엇으로 내보낼지 | 5240 |
| O-02 | 삭제 임계(S-03)를 몇 %로 둘지 | 5240 |
| O-03 | 동기화 실행 주기와 실패 통지 방법 | 5240 |
| O-04 | 초대 방식에서 직원 지메일 주소를 어디에 보관할지 | 고객사·5240 |
| O-05 | `SERVAREA_ID` 파라미터화(현재 콘솔 조회는 데모 300 고정) | 5240 |

---

## 부록 · 근거 자료

| 자료 | 위치 |
|---|---|
| 준비사항 9항목(결정과 이유) | 콘솔 `/prep` · `app/lib/prep.ts` |
| API 참조 구현 | `docs/api-proposal/` |
| 콘솔 설계 문서 | `docs/superpowers/specs/2026-08-11-google-console-redesign-design.md` |
| 초기 검토(일부 무효 — 상단 정정 주석 참조) | `docs/2026-08-10-google-calendar-sync-plan.md` |
| 제품 리버스 문서 | `kiwibox_eGov4.2/spec-docs`, `spec-db/Table/*` |
| 근태 매뉴얼 | `tna-docs/` |
