KCH그룹 인사총무팀 AX 파일럿 — 2일차

DAY 2 · 2026-05-08

오늘 우리가 만들 것

"비공개 시트의 데이터가 내 서버를 거쳐 화면에 보이고,
화면에서 입력한 한 건이 안전하게 시트에 저장되고,
KCH 메일로 로그인한 사람만 들어올 수 있는"
페이지 1개.

IMPORTANT

전체 구현은 오늘 안에 끝나지 않습니다. 한 사이클이 한 번 돌아가는 것까지가 오늘의 약속입니다.

오늘 만든 보안 구조(비공개 시트 + 서버 어댑터 + 도메인 lock)는 교육 후 실 데이터로 그대로 옮겨갈 수 있습니다.

2일차 시간표

45min§A시트 + 계정
30min§B어댑터
60min§C핵심 페이지
60min점심12:30~13:30
60min§D로그인
60min§E폴리싱
50min§F통합
10min마감

챕터로 바로 이동

오늘 학생이 직접 만지는 도구

분류도구비중
시트Google Drive · Sheets20%
구글 클라우드GCP Console (OAuth Client 2개 — 시트 접근용 + 로그인용)15%
코드Claude Code (코드는 거의 다 Claude가 적음)40%
배포Vercel Dashboard (환경변수 등록 + Redeploy)15%
터미널npm install · npm run dev · git · curl10%

터미널 명령은 오늘 7개 정도뿐입니다. 부담 갖지 마세요.

A 10:15~11:00 · 45분 ·

§A. 마스터 시트 + OAuth 토큰

xlsx → Google Sheet 변환 + OAuth 클라이언트 발급 + Refresh Token 1회 + .env 셋업

왜 이걸 하는가

시트는 비공개로 두되, "내 서버만" 읽고 쓸 수 있게 해야 합니다. 그 방법이 OAuth 2.0 Refresh Token — 본인 KCH 계정의 권한을 우리 서버에 한 번만 위임하는 토큰입니다. 일종의 "내가 본인 대신 시트를 보고 쓰도록 발급한 위임장" 같은 것입니다.

이 위임장을 한 번만 받아 서버에 두면, 코드는 본인 권한으로 본인 시트에 안전하게 접근할 수 있습니다. "웹에 게시" 같은 공개 URL은 한 번도 사용하지 않습니다.

왜 서비스 계정 + JSON 키 방식이 아닌가: Google이 2024년 5월부터 신규 조직에 서비스 계정 키 발급을 기본 차단합니다. 정책 해제는 회사 보안 약화이며, 대기업 보안팀이 거부하는 게 정당합니다. OAuth 위임은 본인 계정에 묶여 있어 폐기·감사가 자연스럽고, 보안팀의 추가 승인 없이 진행할 수 있습니다.

이번 단계에서 집중할 것

① 마스터 시트 + 입력 시트 2장을 비공개 상태로 유지
② OAuth 동의 화면(Internal) + 클라이언트 ID 발급
③ OAuth Playground에서 Refresh Token 1회 발급 → .env.local에 5줄 등록

잠시 잊어도 되는 것

시트 컬럼 정렬, 색상, 권한 분기. 시트 공유 단계 자체도 불필요합니다 — 본인 계정 권한으로 본인 시트에 접근하기 때문입니다. 나중에 운영 단계에서 시스템 전용 계정으로 토큰을 다시 발급하면 됩니다.

전체 흐름 한눈에

이번 단계가 끝나면 본인 PC에 다음이 갖춰져 있습니다.

  • 비공개 Google Sheet 2장 (마스터 + 입력)
  • OAuth 클라이언트 ID + Client Secret + Refresh Token 1세트
  • .env.local 파일에 OAuth 자격증명 3개 + 시트 ID 2개, 총 5줄

단계별 진행

01마스터 시트 변환 (xlsx → Google Sheet) 3분

본인 모듈의 마스터 데이터(직원 목록, KPI 목록, 시설 목록, 도구 목록 등)는 지금 xlsx 파일로 받아두었습니다. 이걸 Google Sheet 형식으로 한 번 변환하면, 코드에서 인터넷으로 접근할 수 있는 자리에 옮겨집니다.

  1. Google Drive에서 본인 모듈의 xlsx 파일을 우클릭
  2. 메뉴에서 연결 프로그램 → Google Sheets 선택
  3. 변환된 시트가 새 탭에서 열림. 시트 우상단 제목을 본인 모듈 이름으로 변경 (예: "M3-master-김경호")
이때 보이는 것: 새 시트 탭이 열리며 데이터가 그대로 보입니다. 원본 xlsx도 Drive에 남아 있습니다.
02입력용 시트 새로 만들기 2분

마스터 시트는 "이미 있는 데이터를 읽기만" 합니다. 화면에서 폼으로 입력한 값은 다른 시트에 쌓이는 게 깔끔합니다. 이걸 입력 시트라고 부르겠습니다.

  1. Google Drive 좌상단 + 새로 만들기 → Google Sheets → 빈 스프레드시트
  2. 이름: _inputs_{본인 모듈} (예: _inputs_m3-performance)
  3. 1행에 헤더 입력 (본인 모듈에 맞게)

본인 모듈별 입력 시트 헤더 예시:

학생입력 시트 헤더 (1행에 그대로 입력)
이재성 (M2)timestamp | empId | usedDate | days | reason
김경호 (M3)timestamp | empId | kpiName | weight | status
이동연 (M1)timestamp | empId | facilityId | startDate | persons
이윤재 (M4)timestamp | toolName | toolUrl | owner | status
이때 보이는 것: 빈 시트 1장 + 첫 행에 컬럼명 5개. 그 외에는 아무것도 없습니다.
03두 시트의 ID 메모하기 1분

각 시트의 주소창을 보면 다음과 같은 긴 URL이 있습니다.

https://docs.google.com/spreadsheets/d/1AbcDefGhi-...장-긴-문자열.../edit

/d//edit 사이의 긴 문자열이 시트 ID입니다. 메모장에 두 시트 ID를 모두 복사해두세요. 잠시 후 .env.local에 들어갑니다.

메모장 예시

마스터 시트 ID: 1AbcDefGhi...
입력 시트 ID: 1XyzPqrStu...

04OAuth 동의 화면 설정 (GCP Console) 5분

이제 GCP Console에 가서 "우리 앱이 본인 시트에 접근해도 될까요?"라고 본인에게 물어보는 동의 화면을 먼저 만듭니다. 비유하자면 위임장을 발급할 때 쓸 양식지를 만드는 것과 같습니다.

  1. GCP Console에 접속 → 본인 어제 만든 프로젝트 선택
  2. 좌측 메뉴에서 API 및 서비스 → OAuth 동의 화면
  3. User Type: 내부(Internal) 선택 → 만들기
    • KCH 도메인 내부 사용자만 동의할 수 있는 모드. 게시 절차 없이 바로 사용 가능하고 가장 안전합니다.
  4. 앱 이름: kch-day2-sheets
  5. 지원 이메일·개발자 연락처 이메일: 본인 KCH 메일
  6. 저장 후 계속 → 범위(Scopes) 단계에서 "범위 추가 또는 삭제" 클릭
  7. 필터에 spreadsheets 입력 → https://www.googleapis.com/auth/spreadsheets 체크 → 업데이트
  8. 저장 후 계속 → 요약 화면에서 마무리
이때 보이는 것: OAuth 동의 화면이 "사용 중" 상태가 되고, 범위 목록에 .../auth/spreadsheets가 한 줄 추가되어 있습니다.
05OAuth 클라이언트 ID 발급 (데스크톱 앱) 3분

04단계가 동의 양식지였다면, 이번엔 위임장을 발급할 권한을 가진 발행 기관을 만듭니다. 이게 OAuth 클라이언트입니다.

  1. 좌측 메뉴 API 및 서비스 → 사용자 인증 정보
  2. 상단 + 사용자 인증 정보 만들기 → OAuth 클라이언트 ID
  3. 애플리케이션 유형: 데스크톱 앱 (서버에서 토큰을 새로고침할 때 가장 단순한 형태)
  4. 이름: kch-day2-cli → 만들기
  5. 팝업에 표시되는 Client IDClient Secret을 메모장에 복사
중요 — Client Secret은 비밀번호와 같은 것

이 값이 외부에 노출되면 누구든 본인을 사칭하는 위임장을 만들 수 있습니다. GitHub에 올리지 말 것. 잠시 후 .env.local(.gitignore에 등록되어 push되지 않음)에 들어갑니다.

06Refresh Token 1회 발급 (OAuth Playground) 5분

이제 04·05에서 만든 양식지·발행 기관을 이용해 실제 위임장(Refresh Token)을 한 번만 받습니다. Google이 제공하는 OAuth Playground 도구를 사용합니다.

  1. OAuth Playground 접속
  2. 우상단 톱니바퀴 ⚙"Use your own OAuth credentials" 체크
  3. 05단계의 Client ID / Client Secret 입력 → Close
  4. 좌측 API 목록에서 Google Sheets API v4 펼침 → https://www.googleapis.com/auth/spreadsheets 체크
  5. Authorize APIs 클릭 → 본인 KCH 계정 로그인 → 동의
  6. 화면이 Step 2로 이동하면 Exchange authorization code for tokens 클릭
  7. 응답 패널에 나타난 Refresh token 값을 메모장에 복사
    • 1//0g... 형식으로 시작하는 긴 문자열입니다.
이때 보이는 것: Refresh token + Access token 두 줄이 응답에 표시됩니다. Refresh token만 메모장에 복사하면 됩니다 (Access token은 1시간 만료라 서버가 자동으로 갱신함).
시트 공유 단계가 없는 이유

이 토큰은 본인 KCH 계정의 권한을 그대로 위임받은 것입니다. 본인이 이미 시트의 소유자/편집자이므로 별도로 누구에게 공유할 필요가 없습니다 — 서비스 계정 방식과 가장 큰 차이입니다.

07.env.local 작성 3분

본인 repo 루트에 .env.local 파일이 이미 있을 수도 있고 없을 수도 있습니다. 없으면 새로 만들고, 있으면 다음 5줄을 추가합니다.

# .env.local (repo 루트) GOOGLE_OAUTH_CLIENT_ID=...apps.googleusercontent.com GOOGLE_OAUTH_CLIENT_SECRET=GOCSPX-... GOOGLE_OAUTH_REFRESH_TOKEN=1//0g... SHEET_ID_MASTER=1AbcDef... SHEET_ID_INPUT=1XyzPqr...

각 값은 05단계의 Client ID/Secret, 06단계의 Refresh Token, 03단계의 두 시트 ID로 채웁니다. .env.local 파일이 .gitignore에 들어 있는지 한 번 확인:

cat .gitignore | grep env
출력에 .env.local이 보이면 OK — push 시 자격증명이 GitHub에 안 올라갑니다.
§A 미니 체크리스트
  • xlsx → Google Sheet 변환 (마스터)
  • _inputs_{모듈} 시트 + 헤더 1행
  • 두 시트의 ID 메모
  • OAuth 동의 화면(Internal) + spreadsheets 범위 추가
  • OAuth 클라이언트 ID(데스크톱 앱) 발급 → Client ID/Secret 메모
  • OAuth Playground에서 Refresh Token 1회 발급
  • .env.local에 5줄(OAuth 3 + 시트 ID 2) 등록
B 11:00~11:30 · 30분 ·

§B. Sheets 어댑터 + API Route

서버 코드 한 곳에 시트 호출 규칙을 모아두기

왜 이걸 하는가

브라우저에서 시트를 직접 부르면 §A에서 받은 OAuth 자격증명이 바깥에 노출됩니다. 그래서 규칙을 단순하게 잡습니다 — "브라우저는 본인 서버에만 말을 걸고, 서버가 시트를 다룬다."

이 규칙을 한 곳에 모아둔 게 어댑터(src/lib/sheets.ts)와 API Route(src/app/api/sheets/[name]/route.ts). 한 번 만들어두면 모든 페이지가 이 한 곳에 부탁만 합니다 — 페이지마다 시트 코드를 새로 짤 일이 없어집니다.

이번 단계에서 집중할 것

① 어댑터 함수 3개 — getClient, readRows, appendRow
② API Route 1개 — /api/sheets/[name] (GET=읽기, POST=쓰기)
③ 코드는 Claude가 다 적습니다. 본인은 응답이 정상인지만 눈으로 확인합니다.

잠시 잊어도 되는 것

인증 미들웨어, 캐시, 정교한 에러 처리. 인증은 §D에서, 다른 디테일은 §E에서 추가합니다.

비유로 한 번 더

회사에 인사정보를 보관한 캐비닛이 있다고 생각하세요. 누군가가 인사정보를 보고 싶을 때 캐비닛에 직접 가지 않고, 인사총무팀 담당자에게 전화합니다. 그러면 담당자가 캐비닛에서 자료를 꺼내 답해줍니다. 이때 담당자가 API Route, 캐비닛 열쇠 보관함이 어댑터입니다.

단계별 진행

01패키지 설치 2분

본인 repo 루트의 터미널에서:

npm i googleapis google-auth-library
이때 보이는 것: "added X packages" 메시지가 나오고 package.json의 dependencies에 두 줄이 늘어납니다.
02Claude Code에 어댑터 + API Route 작성 부탁 10분

아래 프롬프트를 본인 repo의 Claude Code 세션에 그대로 붙여넣습니다.

src/lib/sheets.ts에 다음 3개 함수를 만들어줘. - getClient(): google-auth-library의 OAuth2Client에 환경변수 GOOGLE_OAUTH_CLIENT_ID / GOOGLE_OAUTH_CLIENT_SECRET을 넣고, setCredentials({ refresh_token: process.env.GOOGLE_OAUTH_REFRESH_TOKEN })를 호출. 그 OAuth2Client를 auth로 google.sheets({ version: 'v4', auth })를 만들어 반환 - readRows(sheetId, tabName): 해당 탭 전체 행을 읽어 객체 배열로 반환. 첫 행을 헤더로 사용 - appendRow(sheetId, tabName, obj): 헤더 순서대로 obj의 값을 정렬해 한 행 추가 src/app/api/sheets/[name]/route.ts: - GET: name 파라미터가 'master'면 SHEET_ID_MASTER, 'input'이면 SHEET_ID_INPUT을 사용. readRows 호출. JSON 응답 - POST: body의 객체를 input 시트에 appendRow. 응답 {ok:true} - 어떤 에러든 status 500 + {error:string} JSON 반환 - 모든 라우트에 export const runtime = 'nodejs', dynamic = 'force-dynamic'

Claude가 두 파일을 자동으로 만들어줍니다. 본인은 코드를 정독하지 말고, 다음 단계로 바로 넘어가세요. 동작이 곧 검증입니다.

03로컬 서버 실행 + 읽기 검증 5분

터미널에서:

npm run dev

"Ready - started server on http://localhost:3000" 메시지가 보이면, 브라우저에서 다음 주소를 직접 입력합니다.

http://localhost:3000/api/sheets/master
이때 보여야 하는 것: 마스터 시트의 행들이 JSON 배열로 보입니다 (예: [{"empId":"E001","rank":"director",...}, ...]). 빈 배열 []이 나오면 마스터 시트의 첫 탭 이름이 kpi_targets가 아닐 수 있어요 — 어댑터가 어느 탭을 보고 있는지 Claude에게 물어 확인.
에러가 나면

500이나 401이 뜨면 § "막힐 때" 챕터로 가서 원인을 확인합니다 — 보통은 Refresh Token이 잘못됐거나 OAuth 동의 화면 범위에 spreadsheets가 빠진 경우입니다.

04쓰기 검증 (curl 한 줄) 5분

다른 터미널 탭을 열고 (npm run dev는 그대로 둔 채):

curl -X POST -H "Content-Type: application/json" \ -d '{"empId":"E001","kpiName":"테스트","weight":100,"status":"draft"}' \ http://localhost:3000/api/sheets/input

위 JSON은 김경호(M3) 예시입니다 — 본인 모듈의 헤더에 맞게 키를 바꿔 보내세요.

이때 보여야 하는 것: {"ok":true} 응답.
그리고 입력 시트를 새로고침하면 마지막 행에 방금 보낸 데이터가 들어가 있습니다.
§B 미니 체크리스트
  • googleapis·google-auth-library 설치
  • src/lib/sheets.ts 3개 함수 존재
  • /api/sheets/master GET이 JSON 응답
  • /api/sheets/input POST 후 시트에 행 추가
C 11:30~12:30 · 60분 ·

§C. 핵심 페이지 1개

읽기 + 폼 입력. 본인 모듈에서 가장 중요한 페이지 단 한 장.

왜 이걸 하는가

지금까지 §A·§B에서 만든 것이 이 페이지에서 처음으로 화면에 나타납니다. 한 사이클이 한 번 도는 것 — 이게 오늘의 가장 중요한 산출물입니다.

본인 모듈에서 가장 중요한 페이지 단 1개만 끝까지 끌고 가세요. 나머지 페이지는 같은 패턴 복사이기 때문에 30일 안에 채울 수 있습니다. 오늘 4-5개를 동시에 시작하면 1개도 끝까지 못 갑니다.

이번 단계에서 집중할 것

① 마스터 데이터를 화면에 표·카드로 표시
② Loading / Empty / Error 3가지 상태 모두 자연스럽게 보이도록
③ 폼 1개 → 입력 시트에 저장 → 화면 새로고침 → 입력값이 바로 보임

잠시 잊어도 되는 것

다른 페이지, 디자인 디테일. KCH 컬러(#1962A8)만 적용되면 OK.

본인 모듈별 핵심 페이지

학생라우트마스터에서 읽기폼 입력 항목
이재성 (M2)/m2-leaves/employee/[id]직원 1명 + 잔여 표시연차 사용 신청 1건 (날짜·일수·사유)
김경호 (M3)/m3-performance/kpi-input직원의 KPI 목록KPI 1건 (이름·가중치·기한)
이동연 (M1)/m1-facilities/apply시설 5개 카드신청 1건 (시설ID·시작일·인원)
이윤재 (M4)/m4-tools등록된 도구 4건도구 등록 1건 (이름·URL·담당)

단계별 진행

01Claude Code에 페이지 작성 부탁 15분

본인 라우트에 맞춰서 다음 프롬프트를 채우고 Claude에게 전달합니다. {본인 라우트}{본인 필드들}은 위 표에서 본인 행을 보고 채우세요.

{본인 라우트}/page.tsx에 클라이언트 컴포넌트 페이지를 만들어줘. [데이터 흐름] - 마운트 시 fetch('/api/sheets/master') → 응답 JSON 배열을 상태에 저장 - 화면에 카드/표로 표시 (KCH 블루 #1962A8, shadcn Card/Badge 사용) - 하단에 폼 1개 — 입력 항목: {본인 필드들} - 폼 제출 시 fetch('/api/sheets/input', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(폼값)}) - 응답 ok가 true이면 다시 마스터 fetch + 알림 토스트 "저장되었습니다" [상태 UI] - Loading: "데이터를 불러오는 중입니다…" 카드 - Empty (배열 길이 0): "표시할 데이터가 없습니다" 카드 - Error: "데이터를 불러오지 못했습니다 (잠시 후 다시 시도)" 카드 + 새로고침 버튼 [디자인] - 폰트 Pretendard, 행간 1.85 - 컨테이너 max-w-4xl mx-auto, padding 2rem - 카드는 둥근 모서리(0.5rem) + 1px 보더(rgba(11,26,43,0.12))
02로컬 동작 확인 10분
  1. 터미널에서 npm run dev 실행 (이미 떠 있으면 OK)
  2. 브라우저에서 본인 라우트 직접 입력 (예: http://localhost:3000/m3-performance/kpi-input)
  3. 마스터 데이터가 카드/표로 보이는지 확인
  4. 폼에 값을 채우고 제출 → 토스트 "저장되었습니다" 표시
  5. 입력 시트를 다른 탭에서 새로고침 → 마지막 행에 방금 입력한 데이터가 들어 있음
  6. 화면 새로고침 → 입력 데이터가 함께 보임
이때 한 사이클이 완성됩니다: 시트 → 서버 → 화면 → 폼 → 서버 → 시트 → 화면. 이 한 줄이 오늘의 핵심.
03git commit + push → Vercel 자동 배포 10분

로컬에서 정상이면 이제 공개 URL에서도 동작하게 합니다.

git add . git commit -m "feat: §C 핵심 페이지 1개 — 시트 읽기/쓰기 사이클" git push

GitHub repo에 push되면 Vercel이 자동으로 빌드 + 배포합니다 (1~2분 소요).

04Vercel 환경변수 등록 (가장 자주 빠뜨리는 단계) 10분

로컬은 .env.local을 보지만 Vercel은 그 파일을 모릅니다. Vercel Dashboard에 같은 5개 값을 따로 등록해야 합니다.

  1. Vercel Dashboard → 본인 프로젝트 선택
  2. 상단 Settings → Environment Variables
  3. 다음 5개를 한 번씩 추가 (Environment는 모두 Production·Preview·Development 체크)
    • GOOGLE_OAUTH_CLIENT_ID = §A 05단계의 Client ID
    • GOOGLE_OAUTH_CLIENT_SECRET = §A 05단계의 Client Secret
    • GOOGLE_OAUTH_REFRESH_TOKEN = §A 06단계의 Refresh Token
    • SHEET_ID_MASTER = 마스터 시트 ID
    • SHEET_ID_INPUT = 입력 시트 ID
  4. 등록 후 상단 Deployments → 최신 빌드 → ⋯ 메뉴 → Redeploy (환경변수 적용 위해 재빌드 필수)
이때 보여야 하는 것: Vercel Deployments 탭에 "Building → Ready" 상태로 새 빌드 1개가 추가됩니다. 1~2분 후 본인 Vercel URL에서 같은 라우트가 동작합니다.
05Vercel URL에서 한 사이클 검증 5분

본인 Vercel URL에 접속해서 §C 02단계와 동일하게 한 사이클을 한 번 더 돌려봅니다. 로컬에서 됐다고 해도 Vercel에서 한 번 더 검증해야 합니다 — 환경변수 누락은 흔한 실수입니다.

여기까지 오면

오늘의 가장 큰 산출물 "한 사이클 살아 있는 페이지 1개"가 본인 손에 있습니다. 점심 먹고 옵시다.

§C 미니 체크리스트
  • 로컬에서 마스터 데이터 표시
  • 로컬에서 폼 입력 후 입력 시트에 행 추가
  • 새로고침 시 입력값이 함께 보임
  • Vercel env에 3개 값 등록 + Redeploy
  • Vercel URL에서도 한 사이클 동일하게 동작
D 13:30~14:30 · 60분 ·

§D. Google 로그인 + KCH 도메인 lock

버튼 1개로 끝. KCH 메일로만 들어옵니다.

왜 이걸 하는가

지금 본인 Vercel URL은 주소만 알면 누구나 들어옵니다. 인사정보가 화면에 보이는데 외부에 풀려 있는 상태는 사고로 이어집니다. 그래서 페이지에 "KCH 메일로 로그인한 사람만" 들어오게 자물쇠를 답니다.

가장 단순한 길이 Google Identity Services(GIS) 버튼 1개 — Google이 제공하는 로그인 버튼을 그대로 페이지에 넣고, 사용자가 로그인하면 받는 ID 토큰의 hd 클레임이 kchglobal.co.kr인지 확인합니다. 서버 세션·NextAuth 모두 없이 클라이언트 한 곳에서 끝.

이번 단계에서 집중할 것

① §A에서 이미 만든 kch-dashboard 클라이언트 재사용 — 새로 만들지 않음
② 승인된 JS 출처에 http://localhost:3000 + 본인 Vercel URL 있는지 확인·추가
③ 페이지 진입 시 미로그인이면 GIS 버튼만 표시 → 로그인 후 ID 토큰의 hd 검증

잠시 잊어도 되는 것

관리자/일반 권한 분기, 세션 만료 자동 갱신. 새로고침 시 다시 로그인하면 됩니다. 운영 단계로 넘어가면 NextAuth로 교체.

비유

회사 출입문에 카드 리더기를 다는 일과 같습니다. 카드 리더기(GIS 버튼)가 카드(Google 계정)를 읽고, 사번(이메일)이 우리 회사(kchglobal.co.kr) 사람이면 문이 열립니다. 외부인은 입구에서 막힙니다. 사번 발급은 우리가 안 하고 Google이 다 알아서 해주는 게 핵심입니다.

단계별 진행

01기존 클라이언트 ID 확인 + JS 출처 추가 3분

§A에서 Refresh Token 발급에 쓴 kch-dashboard 클라이언트를 그대로 씁니다. 새로 만들지 않습니다.

  1. GCP Console → API 및 서비스 → 사용자 인증 정보
  2. kch-dashboard 클라이언트 클릭
  3. 승인된 JavaScript 출처에 다음이 있는지 확인. 없으면 추가:
    • http://localhost:3000
    • 본인 Vercel URL (예: https://kch-m3-...vercel.app)
  4. 저장 → 우측 패널의 클라이언트 ID 복사 (Client Secret은 이 단계에서 안 씀)
이때 보이는 것: 기존 클라이언트 상세 화면에서 JavaScript 출처 목록에 두 줄이 추가됩니다. 새 항목이 생기지 않습니다.
02.env.local에 환경변수 2개 추가 2분
# .env.local NEXT_PUBLIC_GOOGLE_CLIENT_ID=...apps.googleusercontent.com NEXT_PUBLIC_ALLOWED_HD=kchglobal.co.kr

NEXT_PUBLIC_ 접두사가 붙은 환경변수만 클라이언트(브라우저)에서 읽힙니다. Client ID는 본래 공개되어도 되는 값이라 이 접두사를 씁니다.

03Claude Code에 AuthGate 컴포넌트 작성 부탁 15분
src/components/auth-gate.tsx 클라이언트 컴포넌트를 만들어줘. 요구사항: - next/script로 https://accounts.google.com/gsi/client async defer 로드 - google.accounts.id.initialize({client_id: NEXT_PUBLIC_GOOGLE_CLIENT_ID, callback: handleCredentialResponse, auto_select: false}) - google.accounts.id.renderButton(div, {type:"standard", size:"large"}) - handleCredentialResponse(response): 1) response.credential을 '.'으로 split, 두 번째 조각을 base64url decode → JSON.parse → payload 2) payload.hd === process.env.NEXT_PUBLIC_ALLOWED_HD 인지 확인 3) 같으면 localStorage.setItem('auth_token', response.credential), localStorage.setItem('auth_hd', payload.hd), state isAuthed=true 4) 다르면 alert("KCH 도메인 메일만 접근 가능합니다") + google.accounts.id.disableAutoSelect() - 미로그인이면 children 대신 로그인 카드(중앙 정렬, KCH 블루 강조, 제목 "KCH 도메인 로그인 필요")만 표시 - 로그인 통과 시 children 렌더 + 헤더 우측에 "로그아웃" 버튼 (클릭 시 localStorage 비우고 새로고침) src/app/layout.tsx에서 children을 로 감싸기.
04로컬에서 KCH 메일 vs 외부 메일 검증 10분
  1. npm run dev 재시작 (env 변경 반영)
  2. 로그아웃 상태로 본인 라우트 접근 → 로그인 카드 + Google 버튼만 보임
  3. 본인 GWS 메일로 로그인 → 통과, 페이지 콘텐츠 표시
  4. 로그아웃 버튼 클릭 → 다시 로그인 카드
  5. 이번엔 개인 Gmail로 로그인 시도 → "KCH 도메인 메일만" 알림 + 페이지 안 열림
이때 보여야 하는 것: KCH 메일은 통과, 외부 메일은 거부. 두 경로 모두 직접 한 번씩 확인.
05Vercel env 등록 + Redeploy + 배포 URL 검증 10분
  1. Vercel Dashboard → Settings → Environment Variables → 다음 2개 추가
    • NEXT_PUBLIC_GOOGLE_CLIENT_ID
    • NEXT_PUBLIC_ALLOWED_HD = kchglobal.co.kr
  2. git commit + push → Vercel 자동 빌드 (또는 Deployments에서 Redeploy)
  3. 본인 Vercel URL 접속 → 같은 검증 (KCH 통과 / 외부 거부)
여기까지 오면

본인 페이지가 KCH 도메인 메일로만 들어오는 잠긴 페이지가 됩니다. 외부에 URL이 노출되어도 안전.

§D 미니 체크리스트
  • GCP kch-dashboard 클라이언트에 JS 출처 추가 확인
  • .env.local + Vercel env에 GIS 환경변수 등록
  • 로컬에서 KCH 메일 통과 / 개인 Gmail 거부
  • Vercel URL에서도 동일 동작
  • 로그아웃 후 보호 페이지 접근 시 로그인 카드만 보임
지금 방식의 보안 한계 — 알고 쓰기

지금 구조는 UI만 막고 있습니다. /api/sheets/*는 URL을 직접 호출하면 로그인 없이 데이터를 읽을 수 있는 상태입니다. KCH 내부 인원만 쓰는 시범 운용 단계에서는 충분하지만, 알고 있어야 합니다.

항목지금 GIS 방식NextAuth(Auth.js v5)
토큰 저장localStorage (XSS 취약)HttpOnly 쿠키 (XSS 차단)
만료 관리직접 구현 필요프레임워크가 자동 처리
/api/sheets/* 보호❌ 누구나 직접 호출 가능auth()로 API 라우트 보호
적합한 상황내부 도구, 시연, PoC외부 사용자, 민감 데이터, 운영 서비스

다음 단계로 보안 강화 시 두 가지 선택:

  • API 보호만 추가 (빠름): /api/sheets/*에서 Authorization: Bearer <id_token> 헤더 검증. 클라이언트는 fetch 시 토큰 동봉.
  • NextAuth 마이그레이션 (정석): 쿠키 기반 세션 + hd 검증 + auth()로 API 라우트 보호. 시트 구조는 그대로 유지. → 60일 로드맵 항목.
E 14:30~15:30 · 60분 ·

§E. 핵심 페이지 폴리싱

새 기능 X. 도메인 로직 1줄 + 폼 검증 1개 + 메시지 다듬기.

왜 이걸 하는가

이미 동작하는 페이지를 "발표할 만한 수준"으로 다듬는 시간입니다. 새 페이지를 만들지 마세요. 새 폼을 추가하지 마세요. 본인이 만든 1개 사이클이 빛나도록 손질하는 게 전부입니다.

폴리싱이 끝나면 발표 리허설 1회로 넘어갑니다 — 화면을 띄운 상태에서 "이 모듈은 이런 일을 합니다, 이렇게 동작합니다"를 30초 안에 말로 설명할 수 있어야 합니다.

이번 단계에서 집중할 것

① 본인 도메인 로직 1줄을 화면 한 자리에 명시
② 폼 검증 1개 추가
③ Loading / Empty / Error 한국어 메시지 자연스럽게
④ 발표 1회 리허설

잠시 잊어도 되는 것

다국어, 접근성(ARIA), 모바일 반응형 디테일. 이건 다음 사이클의 일.

본인 모듈별 폴리싱 항목

학생도메인 로직 1줄 (화면 표시)폼 검증 1개
이재성 (M2)잔여 = 법정 + 근속 + 해외 - 사용 = N일 (카드 상단)사용 일수 ≤ 잔여
김경호 (M3)가중치 합계 실시간 표시 (예: "현재 합 80% / 100%")합계 = 100% 일 때만 제출 가능
이동연 (M1)시설 정원 / 현재 신청자 표시 (예: "정원 4명 / 신청 3명")신청 인원 ≤ 정원 - 현재 신청자
이윤재 (M4)등록 도구 수 + 마지막 등록 시각 (헤더)도구 URL이 https로 시작

단계별 진행

01도메인 로직 1줄 추가 15분

화면 상단 또는 폼 위에 위 표의 "도메인 로직 1줄"을 명시합니다. 한 줄짜리 산출 결과를 보여주는 것 — 사용자가 페이지를 보자마자 "이 모듈이 뭘 하는 곳인지" 5초 안에 이해되도록.

{본인 라우트}/page.tsx의 카드 상단에 도메인 로직 1줄을 추가해줘. 표시 내용: {본인 도메인 로직 — §E 표 참고} 스타일: - 큰 숫자(예: "17일") 굵게, KCH 블루 - 산출식(예: "법정 15 + 근속 5 - 사용 3")은 작은 회색 글씨로 그 아래 - 카드는 둥근 모서리(0.5rem) + 1px 보더
02폼 검증 1개 추가 15분

폼이 잘못된 데이터를 받으면 시트가 오염됩니다. 본인 모듈에서 가장 중요한 검증 1개만 추가합니다.

{본인 라우트}/page.tsx의 폼에 검증 1개를 추가해줘. 검증 규칙: {본인 폼 검증 — §E 표 참고} UX: - 검증 실패 시 제출 버튼 비활성화 + 빨간 글씨로 사유 표시 (예: "가중치 합이 100%가 아닙니다 — 현재 80%") - 검증 통과 시 버튼 활성화 - 실시간 검증 (사용자가 타이핑할 때마다)
03Loading / Empty / Error 메시지 다듬기 15분

3가지 상태의 한국어 문구를 본인 모듈에 맞게 자연스럽게 바꿉니다 (모듈 색을 더하면 좋습니다).

상태일반 문구모듈별 다듬기 예시 (M3 김경호)
Loading"데이터를 불러오는 중입니다…""KPI 목록을 불러오는 중입니다…"
Empty"표시할 데이터가 없습니다""등록된 KPI가 없습니다. 첫 KPI를 입력해 주세요."
Error"데이터를 불러오지 못했습니다""KPI를 불러오지 못했습니다. 잠시 후 다시 시도하세요."
04발표 1회 리허설 15분

본인 Vercel URL을 띄운 채로, 다음 30초 발표를 한 번 연습합니다.

  1. 10초: 이 페이지가 무엇인가 (예: "M3 성과관리 — KPI를 입력하고 가중치를 검증하는 페이지")
  2. 10초: 한 사이클 시연 (폼에 1건 입력 → 시트에 저장 → 화면에 보임)
  3. 10초: 핵심 비밀 1개 (도메인 로직 1줄 또는 폼 검증)
리허설 후 점검: 30초 안에 끝나는가? 한 사이클이 빠짐없이 보이는가? 본인 모듈만의 비밀 1개가 명확한가?
§E 미니 체크리스트
  • 도메인 로직 1줄이 화면에 표시됨
  • 폼 검증 1개가 동작 (실패 시 제출 막힘)
  • Loading / Empty / Error 한국어 메시지가 자연스러움
  • 발표 30초 리허설 1회 완료
F 15:30~16:20 · 50분 ·

§F. 4모듈 통합 인덱스

이윤재의 통합 페이지에 4명 카드. 워크숍의 마지막 산출물.

왜 이걸 하는가

지금 4명은 각각 다른 Vercel URL을 가지고 있습니다. 사용자(인사총무팀)는 이 4개 URL을 일일이 기억할 수 없습니다. 그래서 한 화면에서 4명에게 한 번에 들어가는 입구를 만듭니다.

가장 얇은 형태의 통합 — 카드 4개로 묶이는 메타 인덱스(이를 Level 1 Facade 라고 부릅니다). 데이터 통합도, SSO 통합도 아닙니다. 그건 30/60/90일 다음 사이클의 일.

이윤재 모듈(M4 사내도구 카탈로그)이 동시에 통합 인덱스가 됩니다. 본인 시트의 tools_registry에 4명 URL이 들어 있고, 그 4건이 인덱스에 그대로 카드가 됩니다 — 메타-셀프-인덱스.

이번 단계에서 집중할 것

① 다른 3명의 Vercel URL을 받아 tools_registry 시트에 4행 입력
② 통합 인덱스가 카드 4개를 그리는지 확인
③ 4명 동시 시연 — 각자 본인 카드 클릭 → 자기 모듈로 → 1개 동작 → 인덱스로 복귀

잠시 잊어도 되는 것

카드 호버 효과, 애니메이션, 발표 슬라이드. 이 인덱스 페이지 자체가 발표 자료입니다.

이윤재가 진행하는 단계

01통합 인덱스 페이지 만들기 (이윤재) 20분
src/app/page.tsx (이윤재 루트)에 통합 인덱스를 만들어줘. [데이터] - Server Component에서 fetch(BASE/api/sheets/master, {cache:'no-store'}) - 마스터 시트의 tools_registry 탭을 읽고, 4건을 카드로 표시 [카드] - 가로 4열 (모바일은 1열) - KCH 블루(#1962A8) 강조 + shadcn Card - 카드에 표시: 모듈명 / 담당자 / 한 줄 설명 / 마지막 업데이트 - 외부 URL이면 새 탭 (target="_blank"), 내부 라우트(/m4-tools)이면 같은 탭 [페이지 헤더] - 제목: "KCH 인사총무팀 — 사내 도구 인덱스" - 부제: "4명 모듈 통합 진입점"
023명 URL 수집 + tools_registry에 4행 입력 15분

이윤재가 다른 3명에게 본인 Vercel URL을 받아서, M4 마스터 시트의 tools_registry 탭에 직접 4행을 입력합니다 (수동 입력 OK — 오늘은 수동이 정상 흐름입니다).

idnameownerurldesc
m1휴양시설이동연https://...vercel.app휴양시설 신청 + 자격 검증
m2연차이재성https://...vercel.app연차 잔여 자동 산정 + 신청
m3성과관리김경호https://...vercel.appKPI 입력 + 가중치 검증
m4도구 카탈로그이윤재/m4-tools사내 도구 등록 + 통합 인덱스
034명 동시 시연 1회 15분

이윤재의 통합 인덱스를 메인 화면에 띄우고, 4명이 순서대로:

  1. 본인 카드 클릭 → 자기 Vercel URL로 이동
  2. 30초 발표 (§E 04단계의 리허설)
  3. 한 사이클 1번 시연 (폼 입력 → 시트 저장 → 화면 갱신)
  4. "뒤로" 또는 새 탭 닫기 → 통합 인덱스로 복귀
이때 보여야 하는 것: 통합 인덱스에서 4명 카드가 모두 살아 있고, 어느 카드를 클릭해도 그 사람 페이지로 정확히 이동. 4명 모두 시연이 끝나면 워크숍의 메타 산출물이 완성됩니다.
§F 미니 체크리스트
  • tools_registry에 4행 입력
  • 통합 인덱스에 카드 4개 표시
  • 카드 클릭 시 정확히 이동
  • 4명 동시 시연 1회 통과
16:20~16:30 · 10분 ·

마감 — 1줄 회고 + 30/60/90일 로드맵

오늘의 산출물 + 다음 30일에 채울 것

한 줄 회고

각자 한 문장씩 마무리합니다.

회고 양식

  • 오늘 끝낸 1개 사이클: _________________________________
  • 다음 30일에 가장 먼저 채울 것: _________________________________
  • 가장 막혔던 곳 (다음 강의에 보완할 부분): _________________________________

30/60/90일 자력 로드맵

30일

본 모듈 채우기

본인 모듈에 핵심 페이지 1~2개 더 추가. 실 데이터 일부를 시범 운용. 같은 패턴 복붙으로 가능.

60일

정석 인증 + 공통 데이터

GIS 클라이언트 → NextAuth 서버 세션으로 교체. 다른 모듈과 공통 시트(예: org_units) 한 자리에서 관리.

90일

SSO + 자동화 게이트

단일 도메인 + SSO. Vercel Cron Jobs로 자동화 게이트 추가 (분기 메일, 일별 헬스체크 등).

오늘 가져갈 4가지

① 본인 Vercel URL — 한 사이클 살아 있는 페이지 1개
② 비공개 시트 + OAuth 위임 — 회사 보안 정책을 건드리지 않고 실 데이터 운용까지 그대로 쓰는 보안 구조
③ KCH 도메인 lock — 외부 노출 0
CLAUDE.md + repo — 다음 페이지부터는 같은 패턴 복붙

! 참고용 ·

막힐 때 해결법

자주 만나는 7가지 막힘과 해결 순서

먼저 읽어주세요

에러를 만나면 "내가 무엇을 빠뜨렸나?" 보다 "이 에러가 어디서 나오나?"를 먼저 보세요. 브라우저 콘솔(F12), 터미널(npm run dev), Vercel Logs 3곳 중 하나에서 단서가 나옵니다.

01/api/sheets/master 가 401 invalid_grant (토큰 거부) 5분

원인: Refresh Token이 폐기됐거나 잘못 복사됨. 본인이 비밀번호를 변경했거나, OAuth 동의 화면이 External + 테스트 상태로 7일이 경과했거나, Client Secret과 짝이 안 맞는 토큰을 쓰고 있는 경우입니다.

해결: §A 06단계로 돌아가 OAuth Playground에서 Refresh Token을 새로 1회 발급받아 .env.local·Vercel env의 GOOGLE_OAUTH_REFRESH_TOKEN을 교체합니다. 재발급 시 OAuth 동의 화면 User Type이 Internal인지 확인하면 7일 만료 문제가 사라집니다.

02/api/sheets/master 가 403 PERMISSION_DENIED (범위 부족) 5분

원인: 발급받은 토큰의 범위(Scope)에 https://www.googleapis.com/auth/spreadsheets가 빠져 있거나, 본인 계정이 해당 시트의 편집자가 아님.

해결:

  1. OAuth Playground에서 다시 발급할 때 좌측 API 트리에서 Google Sheets API v4 펼침 → spreadsheets 범위가 체크됐는지 확인
  2. 본인이 두 시트(마스터·입력)의 소유자 또는 편집자인지 확인 (본인 권한이 위임되는 구조라 시트 권한이 토큰 권한의 상한)
03로컬은 되는데 Vercel에서 500 5분

원인: Vercel 환경변수 누락 또는 등록 후 Redeploy 안 함.

해결:

  1. Vercel Dashboard → 본인 프로젝트 → Settings → Environment Variables 확인
  2. 5개(GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_CLIENT_SECRET, GOOGLE_OAUTH_REFRESH_TOKEN, SHEET_ID_MASTER, SHEET_ID_INPUT) 모두 등록 + Production·Preview·Development 체크
  3. §D 추가 후라면 NEXT_PUBLIC_GOOGLE_CLIENT_ID, NEXT_PUBLIC_ALLOWED_HD 2개 더
  4. Deployments → 최신 빌드 → ⋯ → Redeploy
04npm run dev 가 env를 못 읽음 3분

원인: .env.local 파일을 만들었거나 수정한 후 dev 서버를 재시작 안 함.

해결: 터미널에서 Ctrl+C로 dev 서버 종료 후 npm run dev 재실행. Next.js는 env를 부팅 시에만 읽습니다.

05GIS 버튼이 안 그려짐 (텅 빈 화면) 5분

원인: Client ID 잘못, 또는 GCP에 JS 출처 등록 안 됨.

해결:

  1. 브라우저 콘솔(F12) 확인 — "Failed to load resource" 또는 "invalid_client" 메시지
  2. GCP Console → 사용자 인증 정보 → 본인 OAuth 클라이언트 → 승인된 JavaScript 출처에 정확히 다음 두 개 등록되어 있어야 함:
    • http://localhost:3000 (포트 정확히)
    • 본인 Vercel URL (https, www 없이)
  3. 변경 사항 반영에 5분 정도 시간 차 있을 수 있음
06개인 Gmail로도 로그인이 통과되어버림 3분

원인: ID 토큰의 hd 클레임 검증 코드가 빠짐.

해결: AuthGate 컴포넌트의 handleCredentialResponse 함수가 다음 로직을 포함하는지 확인:

const payload = JSON.parse(atob(response.credential.split('.')[1])); if (payload.hd !== process.env.NEXT_PUBLIC_ALLOWED_HD) { alert('KCH 도메인 메일만 접근 가능합니다'); return; }

없으면 Claude에게 "AuthGate에 hd 클레임 검증을 추가해줘"라고 요청.

07폼 제출은 됐는데 화면이 안 갱신됨 3분

원인: 응답 받은 후 데이터 재로드 또는 router.refresh() 호출 빠짐.

해결: 폼 제출 핸들러가 다음 패턴인지 확인:

const res = await fetch('/api/sheets/input', { method:'POST', ... }); const json = await res.json(); if (json.ok) { // 데이터 다시 fetch + state 갱신 const fresh = await fetch('/api/sheets/master').then(r => r.json()); setRows(fresh); toast('저장되었습니다'); }

또는 Server Component 페이지에서 useRouter().refresh() 호출.

M1 이동연 — 휴양시설 통합관리

M1. 이동연 — 휴양시설

5지역 시설 + 신청 자격·페널티 검증

오늘 만들 1개

본인 라우트: /m1-facilities/apply
한 사이클 — 시설 5개 카드 표시 + 신청 폼 1개 (시설ID·시작일·인원) → 입력 시트 저장 → 화면에 신청 추가됨.

마스터 시트에 들어갈 데이터 (예시)

역할대표 컬럼
facilities5지역 시설 마스터id | name | location | capacity | maxStay
bookings예약 이력 (수동 입력 OK)empId | facilityId | startDate | persons | status

본인 도메인 로직 1줄 (§E)

"평창 시설 — 정원 4명 / 현재 신청 3명"처럼 정원과 현재 신청자 수를 카드 상단에 명시.

본인 폼 검증 (§E)

신청 인원 (정원 - 같은 날짜 기존 신청자). 초과 시 제출 버튼 비활성화 + "정원 초과" 빨간 메시지.

30일 후 채울 것 (참고)

  • 시드 기반 결정적 추첨 함수 — 같은 시드 → 같은 결과
  • 페널티 룰 (6개월 2건 → 1년 금지)
  • 유지보수 일정 페이지
M2 이재성 — 연차 자동 산출

M2. 이재성 — 연차

법정 + 근속 + 해외 잔여 자동 산정 + 사용 신청

오늘 만들 1개

본인 라우트: /m2-leaves/employee/[id]
한 사이클 — 직원 1명 선택 → 잔여 = 법정+근속+해외 자동 표시 + 연차 사용 신청 폼 (날짜·일수·사유) → 입력 시트 저장 → 잔여 차감 표시.

마스터 시트에 들어갈 데이터 (예시)

역할대표 컬럼
employees직원 마스터 (토큰)empId | rank | deptId | joinDate | isOverseas
leave_balances회계년도별 잔여empId | fiscalYear | legal | seniority | overseas
leave_usages사용 이력 (수동 입력 OK)empId | usedDate | days | reason

본인 도메인 로직 1줄 (§E)

카드 상단에 "잔여 17일" 큰 글씨, 그 아래 "법정 15 + 근속 5 + 해외 0 - 사용 3" 산출식.

본인 폼 검증 (§E)

사용 일수 잔여. 초과 시 "잔여 부족" 빨간 메시지 + 제출 차단.

30일 후 채울 것 (참고)

  • 4개 사업부 카드 페이지 (사업부별 집계)
  • 분기 리포트 페이지
  • ERP CSV 업로드 페이지
M3 김경호 — 성과관리

M3. 김경호 — 성과관리

KPI 입력 + 5단계 평가 + 성과급/연봉 산출

오늘 만들 1개

본인 라우트: /m3-performance/kpi-input
한 사이클 — 직원의 기존 KPI 목록 표시 + 새 KPI 입력 폼 (이름·가중치·기한) → 가중치 합 100% 검증 → 입력 시트 저장 → 화면에 새 KPI 추가됨.

샘플 xlsx 제공

본인 모듈에 그대로 부어볼 수 있는 마스터 데이터 샘플이 있습니다 — 김경호-M3성과관리-마스터샘플.xlsx (교안2일차 폴더). Drive에 업로드하고 §A 1단계의 "연결 프로그램 → Google Sheets" 변환을 그대로 따르면 됩니다.

샘플 xlsx의 시트 구성 (6탭)

행수역할
org_units4부서 4개 + 부서장 매핑
employees20director 4 / pm 8 / am 8 — 모두 토큰 (E001~E020)
kpi_targets60직원당 KPI 3건 — 가중치 합 100% (검증 통과 데이터)
eval_5stage2014건 확정 + 6건 미확정 (Empty 상태 시연용)
salary_calc14등급별·직급별 인상률·성과급 자동 산출 결과
_inputs_kpi_targets0입력 시트 양식 참고용 (헤더만)

본인 도메인 로직 1줄 (§E)

폼 위에 "현재 KPI 합계: 80% / 100%" 실시간 표시. 입력 중에 합이 변하면 즉시 업데이트.

본인 폼 검증 (§E)

가중치 합 = 100% 일 때만 제출 가능. 미만이면 "합이 100%에 도달해야 합니다" 빨간 메시지 + 제출 버튼 비활성화.

샘플 데이터의 산출 룰 (KCH 공식 가정)

등급별 기본 인상률·성과급 × 직급별 계수.

등급인상률 기본성과급 기본
EX (탁월)12%300%
VG (우수)9%200%
GD (양호)6%100%
NI (보완)3%50%
UN (미흡)0%0%
직급인상률 계수성과급 계수
director×1.0×1.5
pm×1.0×1.0
am×0.8×0.8

30일 후 채울 것 (참고)

  • 5단계 평가 진행 페이지 (/m3-performance/eval)
  • 등급 산출 결과 페이지 (/m3-performance/result)
  • 성과급/연봉 산출 페이지 (/m3-performance/salary)
  • 본 평가 확정 시 자동 산출 트리거 (Vercel Cron 또는 Server Action)
M4 이윤재 — 사내도구 + 통합 인덱스

M4. 이윤재 — 사내도구 카탈로그 + 통합 인덱스

본인 모듈 + 4명 통합 진행자 (이중 역할)

오늘 만들 1개

본인 라우트: /m4-tools (도구 카탈로그) + / (4모듈 통합 인덱스 — §F)
한 사이클 — 등록된 도구 4건 표시 + 새 도구 등록 폼 (이름·URL·담당) → URL https 검증 → 입력 시트 저장 → 화면에 추가.

이중 역할

이윤재 모듈은 본인 모듈인 동시에 통합 인덱스입니다. tools_registry 시트에 4행이 들어 있고, 그 4건이 통합 인덱스의 카드 4개로 그대로 표시됩니다 — 메타-셀프-인덱스.

마스터 시트에 들어갈 데이터 (예시)

역할대표 컬럼
tools_registry도구 마스터 (4건 = M1~M4)id | name | owner | url | desc
health_logs헬스체크 이력 (수동 입력 OK)toolId | checkedAt | status

tools_registry 4행 (§F에서 입력)

idnameownerurldesc
m1휴양시설이동연이동연 Vercel URL휴양시설 신청 + 자격 검증
m2연차이재성이재성 Vercel URL연차 잔여 자동 산정 + 신청
m3성과관리김경호김경호 Vercel URLKPI 입력 + 가중치 검증
m4도구 카탈로그이윤재/m4-tools (내부)사내 도구 등록 + 통합 인덱스

본인 도메인 로직 1줄 (§E)

페이지 헤더에 "등록 도구 4개 · 마지막 등록 2026-05-08 14:32" 같은 메타 1줄.

본인 폼 검증 (§E)

도구 URL이 https://로 시작하지 않으면 "보안 URL(https)만 등록 가능" 빨간 메시지 + 제출 차단.

30일 후 채울 것 (참고)

  • 도구 상세 페이지 (/m4-tools/[id])
  • 도구 등록 별도 페이지 (/m4-tools/register)
  • 실행 로그 대시보드 (/m4-tools/dashboard)
  • 일별 헬스체크 자동화 (Vercel Cron + Slack Webhook)
  • AGENTS.md 작성 — 4개 에이전트 책임 분리 (개발/디자인/리뷰/운영)

§8. 요구사항 ⑥/⑦ — "도구 카탈로그"에서 "운영 콘솔"로

요구사항 ⑥(유지보수 운영 체계) + 신설 ⑦(직원별 GitHub·배포 권한) 후속 로드맵 · 강사 미팅·질의응답 보충용

한 줄 정의

collaborator는 "repo 접근권한"이고, 우리에게 필요한 건 "도구·배포·권한·헬스의 통합 운영 상태"입니다. 직원의 GitHub 이용·배포 활성상태·외부 서비스 권한까지 묶이지 않으면 퇴사자 권한 회수가 자동으로 닫히지 않습니다.

3개의 분리된 문제

(a) 인벤토리 — 누가 무엇을 만들고 운영 중인가 [tools_registry · ✅ 있음]
(b) 헬스 + 사용량 — 살아 있는가 / 누가 얼마나 쓰는가 [⚠️ 헬스만 있음, 사용량 없음]
(c) 권한 회수 — 퇴사 시 누가 권한을 끊는가 [❌ 없음, §8에서 신설]

01tools_registry 확장 12필드 (4필드 → 12필드) 스키마

워크숍 14h에서 잡은 4필드는 최소선. 운영 콘솔로 격상하려면 다음 12필드가 표준입니다.

그룹필드역할
식별toolId도구 고유 ID
name도구명
lifecycleactive · paused · beta · deprecated
담당자ownerEmail사내 GWS 이메일
githubUser개인 GitHub 핸들 (org 멤버)
GitHub 자산repoOwnerorg 또는 user
repoNamerepo 이름
repoVisibilityprivate · internal · public
collaboratorscollaborator/team 스냅샷
배포 자산deploymentProvidervercel · firebase · cloud-run · ...
deploymentUrl배포 URL
운영 상태lastCommitAtGitHub Webhook으로 갱신
lastDeployAt배포 플랫폼 API로 갱신
lastHealthCheck일별 cron 헬스체크
offboardingStatuspending · done · n/a
02GitHub 관리 — 회사 계정 종류부터 결정 분기점

회사 GitHub이 Personal Account인지 Organization인지에 따라 가능한 방법이 완전히 달라집니다. Personal Account의 collaborator로는 audit log·일괄 권한 회수·멤버 활동 트래킹이 모두 안 되므로 회사용으로는 권장하지 않습니다.

Organization 플랜별 차이 (GitHub pricing 페이지 검증 결과)

기능FreeTeam ($4/user/월)Enterprise Cloud ($21/user/월)
Audit log UI 조회 (180일)⚠️
Audit log API (자동 수집)
SAML SSO (사내 GWS 강제)
SCIM provisioning (퇴사자 자동 제거)
IP allow list
멤버 일괄 제거 (수동)

⚠️ Free Organization도 audit log UI는 일부 보이지만, 자동 수집(API)·SSO·SCIM은 Enterprise Cloud 전용 (GitHub pricing 페이지 검증).

KCH 상황별 권장 매트릭스

회사 상황권장안이유
사내 도구 < 10개, 입퇴사 드묾Team 플랜 + M4 주기 동기화자동화 일부는 M4가 대체
사내 도구 30+개, 다부서 운영Enterprise Cloud + GWS SAML SSO퇴사자 권한 회수까지 자동
지금 Personal Account만 있음먼저 Organization으로 이전(c) 자동화의 전제 조건
035가지 관리 방식 (검증된 공식 문서 링크 포함) 실행안
#관리 방식핵심 결정
1tools_registry 확장위 12필드를 표준 스키마로 — 4명 모듈이 동일 구조에 맞춤
2GitHub Organization + 팀 단위 권한"회사 전용 계정"을 공유 로그인 계정으로 쓰지 말고 Org 운영. 직원은 본인 GitHub 계정으로 참여. collaborator보다 팀 권한이 퇴사·이동에 강함. GitHub Docs ↗
3collaborator REST API (접근권한 한정)목록·권한·추가/삭제 가능. org repo는 team/org/enterprise 권한이 반영된 role_name 제공. 단 "전체 GitHub 이용상태 추적"은 못함. REST collaborators ↗
4배포 활성상태는 별도 APIGitHub repo 접근권한 ≠ Vercel/Firebase/Cloud Run 배포 상태. M4가 GitHub API + 배포 플랫폼 API + 헬스체크 URL을 같이 봐야 함. REST deployments ↗
5퇴사자 회수 3단계(a) GitHub org/team/repo 권한 제거 → (b) Vercel/Firebase/GCP/Slack 외부 서비스 권한 제거 → (c) Secret/API key 회전. Enterprise Cloud면 audit log + SCIM deprovisioning으로 자동화. Audit log ↗ · SCIM deprovisioning ↗
04자동화 게이트 4개 (1개 → 4개로 재확장) cron
#자동화주기입력출력
1헬스체크 (기존)일 1회 09:00도구 URL pinghealth_logs + Slack 다운 알림
2GitHub Webhook 동기화실시간push·release·issuetools_registry.lastCommitAt
3퇴사자 동기화일/주 1회GWS Directory APIowner='left' → 자동 paused + 알림
4배포 동기화 (신규)일 1회 또는 deploy 후Vercel/Firebase API + GitHub Deployments APItools_registry.lastDeployAt + offboarding 미완 표시
05offboarding 4단계 체크리스트 (수동 SOP) SOP

도구 카탈로그 페이지 하단에 박스로 노출. 담당자가 떠날 때 시스템 + 사람 양쪽 확인:

  • 1단계: tools_registry.owner 변경 → 신규 담당자 이메일
  • 2단계: GitHub Org에서 멤버 제거 (Enterprise면 SCIM이 자동)
  • 3단계: repo ownership transfer 또는 archive 결정
  • 4단계: M4 라이프사이클 → paused 또는 신규 owner 등록 후 active 유지
06DOCX에 추가할 요구사항 ⑦ 표준 문장 문서화

요구사항 정의서 .docx에 그대로 붙여 넣을 표준 문장입니다.

요구사항 ⑦ 직원별 GitHub·배포 권한 운영관리

직원이 만든 사내 도구의 GitHub repo, 배포 URL, 담당자, collaborator/team
권한, 최근 commit, 최근 배포, 헬스체크 상태를 한 화면에서 관리한다.
퇴사·부서이동 시 GitHub repo 접근권한, 배포 플랫폼 권한, API key/secret을
회수·교체하는 offboarding 체크리스트를 제공한다.
GitHub Organization/Teams/API를 우선 활용하되, 배포 활성상태는
Vercel/Firebase/GCP 등 실제 배포 플랫폼과 헬스체크 결과를 함께 연동한다.
07미팅에서 이윤재님께 먼저 확인할 5가지 의사결정
#질문영향
1회사 GitHub은 Personal Account인가 Organization인가? Org면 어느 플랜?02 분기점
2GWS와 GitHub의 SSO 연동에 IT/인사총무 동의 가능한가?04 #3 자동화 가능 여부
3현재 사내에 흩어져 있는 도구는 몇 개인가?02 권장안 결정
4M4가 GitHub 멤버십까지 동기화한다면 권한은 누구에게? IT팀 협조?04 #3 책임 경계
5요구사항 ⑥/⑦를 워크숍 14h에서 어디까지 노출할지시연 깊이 결정

30/60/90일 후속 로드맵 (요구사항 ⑥/⑦ 풀 구현 분할)

30일

GitHub 동기화 + 도구 실 등록

GitHub Webhook 실시간 동기화 + 도구 실 등록 가이드 메일. tools_registry 12필드 표준화.

60일

퇴사자 동기화 + 알림 정교화

GWS Directory API 연동 + 알림 임계치 (실패율 > 10% / 응답 > 30s). 배포 플랫폼 API 동기화.

90일

통합 KPI + 인계 SOP

4모듈 사용량 통합 KPI 대시보드 + 인계 SOP 정식 문서화. 운영 콘솔로 완성.

한 줄 결론

collaborator는 권한 회수와 repo 접근 현황 파악에는 쓸 수 있지만, 직원별 "사용/배포 활성상태" 관리의 중심축으로는 부족합니다. 이윤재님 M4는 "도구 카탈로그"에서 한 단계 올려서 GitHub 권한 + 배포 상태 + 헬스체크 + 퇴사자 회수 체크리스트를 묶는 운영 콘솔로 잡는 게 가장 자연스럽습니다.