# 비기능정의서

**버전** 0.1 (2026-08-12) · **상태** 개발 착수용 초안

**이 연동은 여기가 승부처다.** 기능은 "휴가를 캘린더에 옮긴다" 한 줄이지만, 잘못되면
**전 직원 캘린더에서 일정이 한꺼번에 사라지거나**, **권한 밖 인사정보가 새거나**,
**결재도 안 끝난 휴가가 공개**된다. 아래 항목은 있으면 좋은 것이 아니라 없으면 사고가 나는 것이다.

---

## 1. 안전성 — 가장 중요하다

### NFR-S-01 · 지우는 쪽은 언제나 의심한다

동기화는 스냅샷 대조이므로 **"응답에 없다"가 곧 삭제 신호**다. 그런데 응답이 비는 경우는
취소만이 아니다.

| 원인 | 응답 | 오류인가 |
|---|---|---|
| 취소승인 | 그 건만 빠짐 | 아니오 — 정상 |
| 옵션 1426 미표시 전환 | 그 종류가 빠짐 | 아니오 — 정상 |
| **연동 계정의 조직조회권한 축소** | **전건 0** | **아니오 — 정상 응답이다** |
| **조직 개편으로 대상 조직이 사라짐** | **전건 0** | **아니오 — 정상 응답이다** |
| API 오류·타임아웃 | `retCode ≠ 0` | 예 |

**요구사항**

- 삭제 예정 건수가 **현재 이 연동이 만든 이벤트의 30%를 넘으면 중단**하고 담당자에게 알린다.
  임계값은 설정으로 뺀다(기본 30%). → **O-02**
- 중단 시 **아무것도 지우지 않는다.** 일부만 지우고 멈추면 상태가 더 나빠진다.
- 삭제 0건이어야 정상인 회차와 대량 삭제 회차를 로그에서 구분할 수 있어야 한다.

### NFR-S-02 · 실패하면 아무것도 하지 않는다

`retCode ≠ 0`, 타임아웃, 파싱 실패 — 어느 경우든 **캘린더를 건드리지 않는다.**
"일부라도 반영"은 이 연동에서 손해다. 다음 회차에 전체가 다시 맞춰진다.

### NFR-S-03 · 멱등해야 한다

같은 입력으로 두 번 돌려도 결과가 같아야 한다. 중복 생성은 표식
`(servareaId, kind, reqNo)`로 막는다. 회차가 겹쳐 돌지 않도록 **연동함 단위 잠금**을 둔다.

### NFR-S-04 · 우리가 만든 것만 건드린다

표식 `x5240`이 없는 이벤트는 읽지도 지우지도 않는다. 직원이나 인사담당이 같은 캘린더에
손으로 넣은 일정이 있을 수 있다.

---

## 2. 보안·개인정보

### NFR-P-01 · 인터페이스 키가 새도 전사 정보가 나가지 않는다

조회 범위를 요청값이 아니라 **연동 계정의 조직조회권한**이 정한다(기능정의서 R-01).
키는 "누구로서 조회하는가"까지만 정한다. 이것이 이 설계의 보안 근간이다.

- 연동 계정에는 **필요한 조직만** 열어 준다. 전사 권한을 주면 위 방어가 무의미해진다.
- 인터페이스 키는 옵션(`TAA_CAL_INF_YN`)에 두고, 유출 시 **키만 교체**하면 되게 한다.

### NFR-P-02 · OAuth 스코프는 최소로 고정한다

`https://www.googleapis.com/auth/calendar.app.created` **하나만** 쓴다. 이 앱이 만든 보조
캘린더에만 접근하고 담당자의 기존 개인 일정은 읽지 못한다. **스코프를 넓히지 않는다** —
넓히는 순간 동의 화면 경고와 검증 요건이 함께 올라간다.

OAuth 클라이언트는 **고객사가 소유**한다. 워크스페이스 고객사는 대상을 「내부」로 두어
게시·검증·테스터 한도·리프레시 토큰 7일 만료가 모두 없다.

### NFR-P-03 · 비밀값 보관

| 값 | 지금 | 요구 |
|---|---|---|
| 클라이언트 시크릿 | **파일에 평문**(`data/tenants/<id>/google.json`, 0600) | 운영 전환 시 암호화 보관 또는 비밀 관리소로 이동 |
| 리프레시 토큰 | 같음 | 같음 |
| 화면 표시 | 마스킹만 | 유지 — 원문을 화면에 절대 올리지 않는다 |

> 이 항목은 **콘솔의 현재 구현이 운영 기준에 못 미친다는 사실을 그대로 적은 것**이다.
> 뼈대 단계에서 파일 한 개로 시작했고, 아직 옮기지 않았다.

### NFR-P-04 · 직원 개인 주소를 5240이 보관하지 않는다

개인 지메일 조직에서 초대 방식을 고르면 직원 지메일 주소가 필요하다. 지금은 **인사담당이
구글 캘린더 공유 목록에 직접 넣게** 하여 5240이 개인 주소를 저장하지 않는다. 콘솔에서
관리하게 바꾸려면 개인정보 취급 방침이 선행한다. → **O-04**

### NFR-P-05 · 캘린더에 담지 않는 것

사유(`REASON`), 출장지 상세, 교육 비용 등 **본문 성격의 값은 이벤트에 담지 않는다.**
무엇을 감출지의 기준은 옵션 1426 하나여야 한다.

---

## 3. 성능

| # | 항목 | 기준 | 근거 |
|---|---|---|---|
| NFR-F-01 | 조회 API 응답 | 전사 400일 기준 **10초 이내** | 기간 상한이 400일이다 |
| NFR-F-02 | `ORGF_LINE` 호출 비용 | **실데이터 실행 계획 측정 후 확정** | 조직 범위 계산이 **행마다** 함수를 부른다. 직원 수가 많으면 여기서 느려진다 |
| NFR-F-03 | 캘린더 쓰기 | 배치·백오프 적용 | 구글 API 쿼터에 걸리면 회차가 통째로 실패한다 |
| NFR-F-04 | 동기화 1회 | 1,000건 기준 **5분 이내** | 잠금 시간이 길면 다음 회차와 겹친다 |

> **NFR-F-02는 제품 반영 전에 재야 한다.** 넣고 나서 느린 것을 발견하면 되돌리는 비용이
> 훨씬 크다. 하위조직을 쓰지 않기로 한 것과 무관하게 이 비용은 남아 있다 —
> `ORGF_LINE`은 **권한 범위**를 계산하는 쪽에 있다.

참고로 콘솔의 데모 조회는 프로세스 기동에만 4.3초가 든다(측정치). 60초 TTL 캐시와 병렬화로
`/preview`를 35초 → 콜드 7.7초·재방문 0.05초로 줄였다. 제품 API로 옮기면 이 비용은 사라진다.

---

## 4. 운영

### NFR-O-01 · 호출 기록

모든 조회를 `CMMT_MSG_LOG`에 남긴다(누가·언제·무슨 조건·몇 건). **"캘린더에 왜 저 일정이
있느냐"를 되짚는 유일한 근거**다.

동기화 쪽도 회차마다 남긴다 — 생성·수정·삭제 건수, 중단 여부와 사유.

### NFR-O-02 · 실행 주기와 통지

- 주기는 설정으로 뺀다(기본 제안: **1시간**). → **O-03**
- 실패·중단은 담당자 메일로 알린다. **조용히 멈추면 아무도 모른다.**
- 연속 실패 N회면 통지를 묶어 보낸다(같은 메일이 매시간 오면 아무도 안 읽는다).

### NFR-O-03 · 재실행

같은 회차를 다시 돌려도 안전해야 한다(NFR-S-03). 담당자가 수동으로 한 번 돌릴 수 있는
경로를 둔다.

### NFR-O-04 · 시각 기준

기간 창은 **오늘 기준으로 매 실행 시 다시 계산**된다. 서버 시간대는 `Asia/Seoul`로 고정한다.
자정 근처에 도는 회차는 창이 하루 움직인다는 점을 감안한다(S-04와 함께 본다).

---

## 5. 확장성

| # | 항목 | 요구 |
|---|---|---|
| NFR-E-01 | 다중 고객사 | 서비스영역 ID 단위로 설정·토큰·캘린더가 분리된다. 이미 콘솔이 그 구조다 |
| NFR-E-02 | 갈래 추가 | 휴가·출장·교육 외 갈래가 붙어도 매핑 표만 늘도록 둔다 |
| NFR-E-03 | 조회 경로 교체 | 콘솔은 `lib/oracle.ts` 한 파일이 어댑터 경계다. 제품 API로 갈아끼우면 끝난다 |

> **NFR-E-01의 현재 제약** — 콘솔의 데모 조회는 읽기 전용 MCP 가드가 `SERVAREA_ID='300'`
> 리터럴을 요구해, 300 외 연동함은 설정만 채워 두고 조회가 안 된다. 판정은 `demoOnly()`
> 한 곳에 있고 화면에 「5240 자료 읽기」로 노출된다. 제품 API 전환 시 삭제한다.

---

## 6. 접근성·표시

- 캘린더 이벤트는 **알림을 걸지 않는다**(남의 휴가에 알림이 울리면 안 된다).
- `transparency: transparent` — 직원 개인 일정의 "바쁨"을 덮지 않는다.
- 직원은 언제든 **구독 취소**로 빠질 수 있어야 한다. 강제로 붙이지 않는다.

---

## 7. 열린 항목

| # | 항목 |
|---|---|
| O-02 | 삭제 임계값(기본 30% 제안) |
| O-03 | 실행 주기와 실패 통지 방법 |
| O-04 | 초대 방식에서 직원 지메일 주소 보관 위치 |
| NFR-P-03 | 시크릿 보관 방식 — 파일 평문에서 언제 옮길지 |
| NFR-F-02 | `ORGF_LINE` 실측치 — 측정 후 기준 확정 |
