DAY 05 / EXPORT, VERIFY, RELEASE

만든 앱을,
믿고 나눌 수 있게.

내 과제를 CSV로 정리하고, 권한을 검증하고, staging에서 main까지 배포한 뒤3분으로 소개합니다.

08따라 할 단계
02배포 환경·DB
03분 · 최종 시연
04교시 · 실습 중심
DAY 05 / HANDS-ON

5일차 · 내보내기부터 발표까지

1~4일차에 만든 과제 앱을 이어서 내보내기·권한 검사·최종 배포·발표까지 완주한다. 화면의 언어/프런트엔드·서버와 PostgreSQL DB 역할을 기존 프로젝트 사례와 비교하고 검증 가능한 결과를 남긴다.

전날 시작·CSV 완성·제출 양식 ZIP · 4일차 이어 보기

교시주제50분 운영
5일차1교시CSV 내보내기목표5·복원8·구현22·경계검증11·정리4
5일차2교시계정·MFA·보안 검증원리7·A/B/API20·실패/취소10·비밀검사9·정리4
5일차3교시staging→main 배포환경8·PR/Preview15·검증10·main/운영13·정리4
5일차4교시발표·인수인계준비8·발표/짝피드백30·이슈/인수인계8·정리4

4교시×50분 실습과 휴식40분으로 하루4시간을 구성한다. 휴식은1교시후10분·2교시후10분·3교시후20분 예시다. 이론은 각 교시5~8분으로 줄이고 나머지는 구현·검증·발표에 쓴다.

  1. 014일차 앱

    SNS·MFA·DB

  2. 02CSV

    본인 전체 재조회

  3. 03보안

    실제 API·실패

  4. 04릴리스

    staging→main

  5. 05발표

    근거·인수인계

강사 사전 준비

  • 수업용 staging/production Supabase 두 환경과 Vercel Preview/Production을 준비한다. 무료 계정 한도·실습 제공 범위를 수업 전에 확인한다. 기존 운영 프로젝트는 재사용하지 않는다.
  • staging 브랜치를 main에서 미리 생성·push하고 Production Branch=main, 두 환경의 DB·Auth 설정을 대조한다.
  • 가상 A/B 계정·인증앱·발표용 과제와 실제 교실 스프레드시트의 UTF-8/텍스트 가져오기 방식을 리허설한다.
  • 필요한 Google/Kakao/Naver 테스트 계정·동의·고정 어댑터·메일 발신을 미리 점검한다. 미완료는 미확인으로 남긴다.
  • ZIP은 비교용 starter/solution이다. 기존 .git/.env.local을 보존하고 실제 값은 본인 로컬/서비스 설정에만 입력한다.
목차와 처음으로 ↑
DAY 05 / CONNECTED TOPICS

이번 일차의 관련 내용과 선택 심화

이 날의 생성도구·Git·구축 절차·프로젝트 사례를 각 교시에서 연결한다. 핵심은 아래에서 읽고 긴 원문은 선택 심화 링크로 연다.

D5-1 · 과제 앱 본인 과제 CSV 내보내기

이 교시 전체 교재 · 발표로 이동

내보내기는 다운로드 뒤에도 검증한다

  • 현재 검색 배열 대신 본인 전체 과제를 DB에서 새 조회한다.
  • 한글·인용·줄바꿈·수식 위험 입력과 열 범위를 검사한다.
  • 빈 목록·페이지 오류·계정 전환에서 파일 생성 결과를 확인한다.
  • 스프레드시트 열람용 CSV와 원본 복원 백업의 용도를 구분한다.

kysong 사례 · 이번 실습에 적용할 판단

  • 기록: Excel·HWPX 내보내기를 업무 결과물로 확장했다.
  • 판단: 파일 생성 성공 이후 실제 열기·수정 가능 여부도 검사했다.
  • 적용: 과제 CSV도 내려받아 원본 건수·열·문자와 비교한다.

kysong 전체 구축 과정·기술 역할 ↗

선택 심화 · 원문 2개 열기

해당 교시의 설명을 대체하거나 실습 뒤 선택한다. 별도 수업시간을 추가하지 않는다.

연결 내용을 설명하는 강사 메모

기존 파일 업무 사례를5일차 CSV 검증 기준으로 가져온다. 원본 보존과 열람용 변환의 차이를 실제 예시로 설명한다.

D5-2 · 과제 앱 계정별 권한·보안 최종 검증

이 교시 전체 교재 · 발표로 이동

권한·비밀·실패를 증거로 검증

  • 익명·AAL1·AAL2·A/B의 직접 API 요청을 대조한다.
  • HTTP200도0행일 수 있어 영향 행과 원본 불변을 함께 확인한다.
  • UI 역할·user_metadata를 권한 근거로 사용하지 않는다.
  • 로컬·모의·실제·미확인을 구분하고 비밀 없는 근거를 남긴다.

kysong 사례 · 이번 실습에 적용할 판단

  • 기록: 여러 임시저장을 복구할 때 원본 보존과 정합성을 보완했다.
  • 교훈: 다른 사용자의 기록을 섞어 복구하거나 빈값을 AI가 채우면 안 된다.
  • 적용: 계정 전환 중 늦은 응답·CSV 취소까지 보안 검사에 포함한다.

kysong 전체 구축 과정·기술 역할 ↗

선택 심화 · 원문 2개 열기

해당 교시의 설명을 대체하거나 실습 뒤 선택한다. 별도 수업시간을 추가하지 않는다.

연결 내용을 설명하는 강사 메모

보이는 버튼이나 성공 메시지보다 실제 권한 경계와 원본 불변을 평가한다. 미확인을 통과로 바꾸지 않는다.

D5-3 · 과제 앱 staging→main 최종 배포

이 교시 전체 교재 · 발표로 이동

staging→main: Git 기록과 운영 설정 연결

  • staging 브랜치의 Preview와 main의 Production을 구분한다.
  • 두 환경의 URL·DB·설정 이름·검증 commit을 기록한다.
  • PR 검토→병합→새 빌드→실제 URL 재검사를 순서대로 확인한다.
  • READY·CANCELED·SKIPPED·ERROR는 기능 검증 결과와 다르다.

uc-anchor 사례 · 이번 실습에 적용할 판단

  • 기록: PR #720·#722의 로컬 Git 병합 이력과 Thomas dot의 staging/Preview 논의를 확인했다.
  • 범위: 과거 배포 상태는 당시 관찰이며 이번 운영 재검증 결과가 아니다.
  • 적용: 학생 앱의 검증 commit·배포 commit·URL·DB 대상을 직접 남긴다.

uc-anchor 전체 구축 과정·기술 역할 ↗

선택 심화 · 원문 5개 열기

해당 교시의 설명을 대체하거나 실습 뒤 선택한다. 별도 수업시간을 추가하지 않는다.

연결 내용을 설명하는 강사 메모

과거 사례의 성공을 학생 배포의 성공으로 대신하지 않는다. main은 Production 환경의 새 빌드이며 DB 복구는 앱 롤백과 별도다.

D5-4 · 과제 앱 3분 발표와 인수인계

이 교시 전체 교재 · 발표로 이동

만든 과정과 다음 운영을 인수인계

  • 문제·사용자→실제 동작→검증 근거→배포→한계를3분으로 보여준다.
  • README에 실행·설정 이름·SQL 순서·검사·복구 담당자를 적는다.
  • 동료 피드백1개를 다음 이슈로 남기고 미완료 항목을 명시한다.
  • 발표 자료·CSV·캡처에는 가상 데이터만 사용한다.

u-livet 사례 · 이번 실습에 적용할 판단

  • 기록: 로컬 실험·운영 선택·SNS·MFA 후속 보완이 함께 남아 있다.
  • 교훈: 처음 만든 버전과 후속 수정, 실제/모의/미확인을 나눠 설명한다.
  • 적용: 과제 앱의5일 구축 과정과 다음 운영 작업을 재현 가능하게 전달한다.

u-livet 전체 구축 과정·기술 역할 ↗

선택 심화 · 원문 2개 열기

해당 교시의 설명을 대체하거나 실습 뒤 선택한다. 별도 수업시간을 추가하지 않는다.

연결 내용을 설명하는 강사 메모

전체 프로젝트 연표와 자세한 기술 결정은 별도 사례 파일로 옮겼다. 발표에서는 이번 과제 앱과 관련된 판단 한 가지를 근거로 설명한다.

목차와 처음으로 ↑
PROMPT CARDS

Codex 예제 요청3종과 통합 검증

1. CSV·페이지·계정 전환

4일차 과제 체크 앱의 이메일/SNS 로그인, TOTP MFA, 소유자 RLS를 유지하고 CSV 내보내기를 추가해줘.
먼저 현재 파일·DB 정책·세션 전환 처리를 읽고 변경할 파일과 검증 기준을 제시해줘.
검색 화면의 배열 대신 로그인한 본인의 과제 전체를 DB에서 새로 조회해. getUser의 uid와 currentLevel=aal2를 확인하고 user_id 필터와 DB의 소유자 RLS AND restrictive AAL2를 모두 유지해.
500행씩 created_at,id 순으로 페이지 조회하고 exact count와 중복/누락을 검사해. 10,000행 초과·중간 오류·건수 변경은 부분 파일 없이 실패시켜. 동시 편집의 일관된 스냅샷을 보장한다고 설명하지 마.
열은 과제명/과목/마감일/완료만. UTF-8 BOM, CRLF, 모든 셀 큰따옴표, 내부 따옴표 두 번 처리. 수식 위험 시작문자(공백 뒤 = + - @와 전각 변형, 탭/개행)에는 quoted 셀 안에 탭을 앞붙여 스프레드시트 열람 위험을 줄이고 원본 DB는 바꾸지 마. 모든 앱/재저장에 안전하다거나 원본 복원 백업이라고 설명하지 마.
조회 중 progress와 aria-busy, 중복 실행 방지, 완료/오류 알림, Blob URL 해제를 구현해. 계정/토큰 변경·로그아웃은 AbortController와 epoch 확인으로 취소하고 늦은 A 응답을 B 화면에서 다운로드하지 않게 해.
빈 목록·한글·쉼표·따옴표·줄바꿈·수식 위험·1,000행 초과·중간 실패·계정 전환을 검증하고 npm run build 결과와 미확인 범위를 알려줘. 비밀값을 출력하거나 RLS/MFA를 끄지 마.

2. staging→main 최종 릴리스

현재 과제 앱을 staging→main 최종 릴리스할 수 있도록 점검해줘.
git status, remotes, 브랜치, PR base/head와 배포 설정을 먼저 읽어줘. git staging area와 staging 브랜치를 구분해 설명해줘.
feature/csv-release→staging PR에서 검토하고 staging 브랜치는 Preview로 빌드해. 강사가 준비한 별도 수업 staging Supabase와 production Supabase에 같은 검토된 스키마·소유자 RLS·restrictive AAL2가 있는지 먼저 읽기 전용으로 확인해줘. 기존 운영 DB를 초기화하거나 임의 변경하지 마.
VITE_SUPABASE_URL과 VITE_SUPABASE_PUBLISHABLE_KEY의 Preview/Production 매핑, 실제 build commit·URL·DB 프로젝트 이름을 비밀값 없이 기록해. SNS callback/Redirect URL·SMTP·Naver 서버 어댑터도 환경별로 확인해줘.
검증표에 실패나 미확인이 남으면 완료로 표시하지 마. 통과 후 staging→main PR을 준비하고 main Production 재빌드 후 운영 URL에서 로그인/MFA/CRUD/CSV/계정 분리를 다시 확인해.
Preview 검증이 Production의 같은 환경 검증이라고 단정하지 마. 실패 시 이전 앱 배포 복원과 DB 복구가 다름을 설명하고 릴리스 기록 양식을 채워줘. 강제 push·보호 해제·비밀 커밋 없이 진행해줘.

3. 3분 발표·인수인계

완성한 과제 체크 앱의 3분 시연과 인수인계 자료를 만들어줘.
1일차 문제/사용자와 웹 선택→2일차 Git·개선→3일차 DB·Auth→4일차 SNS·MFA→5일차 CSV·검증·배포를 연결해줘.
0:00~0:30 문제/대상, 0:30~1:30 실제 과제 CRUD·검색·CSV, 1:30~2:15 A/B 권한 검증 근거와 MFA, 2:15~2:45 staging/Production URL·commit, 2:45~3:00 한계/다음 기능으로 구성해줘.
실제 확인한 결과와 모의/미확인을 구분하고 비밀키·토큰·TOTP QR/코드·학생 개인정보를 화면/문서에 넣지 마. 발표용 가상 계정을 사용하고 MFA는 청중에게 비밀을 보이지 않게 사전 완료해.
README에는 실행/환경변수 이름/SQL 적용 순서/CSV 열람 범위/테스트 방법/배포·복구·미확인 항목을 적어줘. 동료 피드백 1건을 이슈로 남길 수 있는 양식과 100점 평가표도 포함해줘.

추가: 계정·CSV·배포 통합 검증

내 과제 앱의 최종 검증을 실행 가능한 범위에서 진행하고 checks.csv에 근거를 남겨줘.
가상 A/B 계정, 익명, AAL1과 AAL2로 직접 Data API를 검사해. 본인 CRUD 허용, 타인 조회/수정/삭제의 0행 또는 거절과 원본 불변, user_id 위조 추가/변경 거절을 확인해. UI가 숨겨졌다는 이유로 권한 검사를 통과시키지 마.
CSV가 본인 전체 DB 과제만 새 조회하는지, 검색 결과와 무관한지, 한글/인용/CRLF/위험 입력/빈 목록/1,203행을 처리하는지 검사해. AAL1 내보내기 거절, 페이지 실패와 계정 전환에서 다운로드 0회를 확인해.
비밀은 값 대신 존재 여부와 변수 이름만 보고해. git diff --cached, .gitignore, 번들에 secret/service_role/SMTP/OAuth secret이 없는지 점검해. publishable key는 공개 식별값이며 RLS를 대신하지 않음을 명시해.
빌드·PR·배포상태와 실제 URL 기능 검사를 분리해. 로컬 통과, 모의 통과, staging 실제, Production 실제, 미실행을 구분해 결과/시각/commit/검증자를 기록해. 실패·미확인 항목이 남으면 출시 완료라고 쓰지 마.
목차와 처음으로 ↑
FOLLOW ALONG

E01 · 전날 앱 복원과 두 환경 준비

4일차 완성 앱이 있는 본인 clone 폴더를 연다. ZIP의 starter는 비교용이며 기존 .git과 .env.local을 덮어쓰지 않는다. 강사는 staging 브랜치와 수업용 staging/production Supabase 두 환경을 사전 준비한다. 학생은 main의 최신 이력에서 feature/csv-release를 만들고 로컬 가입·로그인·MFA·과제 CRUD부터 확인한다.

본인 저장소에서 한 줄씩 실행

git status -sb
git fetch origin
git switch main
git pull --ff-only
git switch -c feature/csv-release
npm ci
npm run dev

학생 확인

미커밋 변경을 먼저 정리하고 잘못된 저장소에서 실행하지 않는다. 환경표에 URL·브랜치·DB 이름만 적는다. 두 환경은 같은 DB를 가리키지 않는다.

강사용 설명

1교시50분은 목표5·복원8·CSV구현22·경계검증11·정리4다. 두 Supabase 프로젝트/키 준비가 지연되지 않게 강사가 별도 수업 프로젝트를 미리 확보한다. 계정 무료 프로젝트 한도를 수업 전에 확인하고 필요하면 강사 제공 환경으로 진행한다. 실제 프로젝트의 운영 DB를 재사용하지 않는다.

목차와 처음으로 ↑
FOLLOW ALONG

E02 · 본인 전체 과제를 CSV로 내보내기

요청1을 Codex에 입력하고 변경 파일을 검토한다. solution의 csv-export.js·task-store.js·main.js·HTML/CSS와 비교한다. 추가 인증 후 내 과제 CSV 내보내기를 누른다. 검색 결과가 1건이어도 본인 전체 DB 과제가 들어가는지 확인한다. 내려받은 파일은 스프레드시트에서 텍스트로 가져오고 한글·열 이름·완료 상태를 확인한다.

이 단계의 Codex 요청

4일차 과제 체크 앱의 이메일/SNS 로그인, TOTP MFA, 소유자 RLS를 유지하고 CSV 내보내기를 추가해줘.
먼저 현재 파일·DB 정책·세션 전환 처리를 읽고 변경할 파일과 검증 기준을 제시해줘.
검색 화면의 배열 대신 로그인한 본인의 과제 전체를 DB에서 새로 조회해. getUser의 uid와 currentLevel=aal2를 확인하고 user_id 필터와 DB의 소유자 RLS AND restrictive AAL2를 모두 유지해.
500행씩 created_at,id 순으로 페이지 조회하고 exact count와 중복/누락을 검사해. 10,000행 초과·중간 오류·건수 변경은 부분 파일 없이 실패시켜. 동시 편집의 일관된 스냅샷을 보장한다고 설명하지 마.
열은 과제명/과목/마감일/완료만. UTF-8 BOM, CRLF, 모든 셀 큰따옴표, 내부 따옴표 두 번 처리. 수식 위험 시작문자(공백 뒤 = + - @와 전각 변형, 탭/개행)에는 quoted 셀 안에 탭을 앞붙여 스프레드시트 열람 위험을 줄이고 원본 DB는 바꾸지 마. 모든 앱/재저장에 안전하다거나 원본 복원 백업이라고 설명하지 마.
조회 중 progress와 aria-busy, 중복 실행 방지, 완료/오류 알림, Blob URL 해제를 구현해. 계정/토큰 변경·로그아웃은 AbortController와 epoch 확인으로 취소하고 늦은 A 응답을 B 화면에서 다운로드하지 않게 해.
빈 목록·한글·쉼표·따옴표·줄바꿈·수식 위험·1,000행 초과·중간 실패·계정 전환을 검증하고 npm run build 결과와 미확인 범위를 알려줘. 비밀값을 출력하거나 RLS/MFA를 끄지 마.

학생 확인

한글·쉼표·큰따옴표·줄바꿈·=1+2·전각=1+2와 빈 목록을 검사한다. 위험한 자유 입력 앞의 탭은 열람용 변환이며 DB 원본은 유지된다. 토큰·이메일·uid가 CSV에 없다.

강사용 설명

CSV 인용은 형식 보존, 수식 위험 변환은 별도 보안 처리다. 탭 접두어는 Excel 열람의 위험을 줄이지만 모든 스프레드시트와 재저장에 대한 보장은 아니다. 코드 검증과 실제 교실 Excel/Sheets에서 텍스트 가져오기 검증을 나눠 기록한다. UI 입력이 trim하는 경우 줄바꿈/선행 제어문자 검사는 코드 테스트로 보충한다.

목차와 처음으로 ↑
FOLLOW ALONG

E03 · A/B·익명·인증 강도별 권한 검사

가상 A/B 계정을 서로 다른 브라우저 프로필에서 사용한다. 두 계정 모두 TOTP를 준비하고 본인 과제를 하나씩 만든다. A의 id를 검사에만 사용해 B의 실제 Data API로 조회·수정·삭제·user_id 위조를 시도한다. 로그아웃 뒤 재로그인한 AAL1과 추가 인증 후 AAL2를 비교한다. 아래 개발 서버 전용 검사 예시와 checks.csv에 실제 결과를 적는다.

학생 확인

AAL2는 관리자 권한이 아니다. B의 A행 조회/수정/삭제는0행 또는 거절이며 A원본 불변. 익명은 거절. AAL1은 본인 조회0행·쓰기거절/0행이며 내보내기 불가. 소유권 위조 추가/변경은 거절.

강사용 설명

2교시50분은 원리7·A/B/API20·실패/취소10·비밀검사9·정리4다. 표의 기대 결과를 읽는 것과 실제 요청 성공/거절을 구분한다. HTTP200의 빈 배열도 RLS 차단일 수 있으므로 반환 행과 A원본을 함께 확인한다. 이메일이 같음/화면의 관리자 표기/user_metadata.role을 권한 근거로 쓰지 않는다.

목차와 처음으로 ↑
FOLLOW ALONG

E04 · 페이지 실패·계정 전환·비밀 노출 점검

네트워크 차단 또는 모의 테스트로 CSV 조회 중간 실패를 재현한다. 다운로드가 한 번도 발생하지 않아야 한다. 조회 중 로그아웃/다른 계정으로 전환했을 때 이전 계정 파일이 생성되지 않는지도 검사한다. 로컬 가상 데이터1,203건으로 페이지 조회를 확인한다. .gitignore·git diff·빌드 산출물을 검사하고 실패/미확인 사유를 남긴다.

본인 저장소에서 한 줄씩 실행

npm run build
git status --short
git diff --check
git diff -- src index.html styles.css
git ls-files .env.local .env.production

학생 확인

부분 CSV·이전 계정 CSV·중복 다운로드0회. 예제 상한10,000건을 넘으면 명시 오류. 위 git ls-files 결과는 비어야 한다. 공개 URL/key 외 secret·service_role·SMTP·OAuth 비밀이 소스/번들에 없다.

강사용 설명

1,203건은 기능 테스트가 끝나면 가상 수업 DB에서만 정리한다. 화면 목록의1,000건 한도와 CSV10,000건 한도를 구분한다. 다른 탭의 동시 편집은 잠시 멈춘다. 페이지 조회는 단일 트랜잭션 스냅샷이 아니며 동일 건수의 동시 편집까지 완전 감지하는 구현이 아니다. 실패를 RLS 해제로 해결하지 않는다.

목차와 처음으로 ↑
FOLLOW ALONG

E05 · staging Preview와 DB·Auth 설정 대조

환경표를 채운다. Vercel Production Branch=main을 확인하고 staging은 Preview로 연결한다. Preview의 VITE_ 두 값은 별도 staging Supabase, Production 값은 production Supabase로 지정한다. 각 새 수업 DB를 inspect.sql로 확인하고 검토한 setup.sql→mfa-required.sql을 한 번 적용한다. 환경별 Auth·메일·SNS callback/redirect·Naver 어댑터를 점검한다.

이 단계의 Codex 요청

현재 과제 앱을 staging→main 최종 릴리스할 수 있도록 점검해줘.
git status, remotes, 브랜치, PR base/head와 배포 설정을 먼저 읽어줘. git staging area와 staging 브랜치를 구분해 설명해줘.
feature/csv-release→staging PR에서 검토하고 staging 브랜치는 Preview로 빌드해. 강사가 준비한 별도 수업 staging Supabase와 production Supabase에 같은 검토된 스키마·소유자 RLS·restrictive AAL2가 있는지 먼저 읽기 전용으로 확인해줘. 기존 운영 DB를 초기화하거나 임의 변경하지 마.
VITE_SUPABASE_URL과 VITE_SUPABASE_PUBLISHABLE_KEY의 Preview/Production 매핑, 실제 build commit·URL·DB 프로젝트 이름을 비밀값 없이 기록해. SNS callback/Redirect URL·SMTP·Naver 서버 어댑터도 환경별로 확인해줘.
검증표에 실패나 미확인이 남으면 완료로 표시하지 마. 통과 후 staging→main PR을 준비하고 main Production 재빌드 후 운영 URL에서 로그인/MFA/CRUD/CSV/계정 분리를 다시 확인해.
Preview 검증이 Production의 같은 환경 검증이라고 단정하지 마. 실패 시 이전 앱 배포 복원과 DB 복구가 다름을 설명하고 릴리스 기록 양식을 채워줘. 강제 push·보호 해제·비밀 커밋 없이 진행해줘.

학생 확인

환경 변수 변경 뒤 새 빌드를 만든다. 프런트엔드 URL/key 외 비밀값은 VITE_로 만들지 않는다. DB migration은 Vercel 코드 배포와 별도다. 기존 정책이 있으면 SQL을 무작정 재실행하지 않는다.

강사용 설명

3교시50분은 환경8·PR/Preview15·검증10·main/운영13·정리4다. Git의 staging area는 커밋 대기 영역, staging 브랜치는 팀이 정한 이름이다. 브랜치 이름만으로 Vercel Production이 되지 않는다. 새 프로젝트의 Auth 사용자는 독립적이므로 같은 사람이 두 DB에서 다른 uid를 가질 수 있다. 운영 데이터 복제 없이 가상 계정으로 검증한다.

목차와 처음으로 ↑
FOLLOW ALONG

E06 · feature→staging 검토 후 main 최종 배포

변경을 커밋·push하고 GitHub PR base=staging, compare=feature/csv-release를 만든다. 동료가 diff·비밀·검사표를 검토하고 병합한다. Vercel staging Preview의 READY와 commit을 확인한 뒤 실제 URL에서 검증한다. 모두 통과하면 PR base=main, compare=staging을 검토·병합한다. main의 새 Production 배포에서 같은 검사를 다시 하고 release-record.md를 완성한다.

본인 저장소에서 한 줄씩 실행

npm run build
git add index.html styles.css src README.md
git diff --cached
git commit -m "Add secure task CSV export"
git push -u origin feature/csv-release

학생 확인

PR별 base/head·commit·실제 URL·DB 이름·검증자·결과가 남는다. Preview 통과와 Production 재빌드 통과를 별도로 기록한다. 실패하면 배포 완료 표시를 멈추고 원인을 수정한다.

강사용 설명

수업용 staging 브랜치는 시작 전에 main에서 생성해 원격에 둔다. main 병합은 Production 환경으로 새 빌드하므로 preview와 동일 실행물/설정이라고 단정하지 않는다. 배포 롤백은 앱 버전 복원이며 이미 저장한 DB 데이터나 스키마를 자동 복원하지 않는다. 잘못된 데이터 변경은 별도 검토된 DB 복구 절차가 필요하다.

목차와 처음으로 ↑
FOLLOW ALONG

E07 · 3분 발표·짝 피드백

요청3으로 발표 원고를 작성하고 발표용 가상 과제3개와 CSV를 준비한다. 청중에게 비밀이 보이지 않게 MFA를 사전 완료한다. 문제/사용자→핵심 동작→권한 검증 근거→두 배포 환경→한계 순서로3분 시연하고 짝에게 개선점1개를 받는다. 실SNS 연결 미완료는 미확인으로 설명한다.

이 단계의 Codex 요청

완성한 과제 체크 앱의 3분 시연과 인수인계 자료를 만들어줘.
1일차 문제/사용자와 웹 선택→2일차 Git·개선→3일차 DB·Auth→4일차 SNS·MFA→5일차 CSV·검증·배포를 연결해줘.
0:00~0:30 문제/대상, 0:30~1:30 실제 과제 CRUD·검색·CSV, 1:30~2:15 A/B 권한 검증 근거와 MFA, 2:15~2:45 staging/Production URL·commit, 2:45~3:00 한계/다음 기능으로 구성해줘.
실제 확인한 결과와 모의/미확인을 구분하고 비밀키·토큰·TOTP QR/코드·학생 개인정보를 화면/문서에 넣지 마. 발표용 가상 계정을 사용하고 MFA는 청중에게 비밀을 보이지 않게 사전 완료해.
README에는 실행/환경변수 이름/SQL 적용 순서/CSV 열람 범위/테스트 방법/배포·복구·미확인 항목을 적어줘. 동료 피드백 1건을 이슈로 남길 수 있는 양식과 100점 평가표도 포함해줘.

학생 확인

저장소/운영 링크·화면 시연·권한 근거·staging/Production commit·다음 개선1개가 있다. QR·OTP·토큰·실제 학생 정보가 발표 화면/CSV/캡처에 없다.

강사용 설명

4교시50분은 준비8·발표/짝피드백30·이슈/인수인계8·정리4다. 3분 발표+2분 피드백을6명 소그룹에서 병렬 진행하고 대표 사례만 전체 공유한다. 대규모 수업에서 전원이 전원 앞 발표하면 시간을 넘는다. “AI가 만들었다”보다 문제와 검증 근거를 평가한다.

목차와 처음으로 ↑
FOLLOW ALONG

E08 · 인수인계·릴리스 기록·다음 이슈

README에 실행/환경 변수 이름/SQL 순서/검사 방법/CSV 범위/배포·복구를 정리한다. checks.csv와 release-record.md를 채우고 peer-feedback.md의 개선점 하나를 GitHub 이슈로 남긴다. 최종 main commit·운영URL·발표 원고·가상 CSV·미확인 항목을 제출한다.

본인 저장소에서 한 줄씩 실행

git status -sb
git log -5 --oneline
git diff --check

학생 확인

본인이 하지 않은 실SNS·운영 검증을 통과로 표시하지 않는다. 발표용 CSV도 본인 가상 자료만 제출한다. 미커밋 작업·열린 PR·실패 검사·복구 담당자가 분명하다.

강사용 설명

성공 기준은 공개 링크 하나가 아니라 재현 가능한 실행·권한·배포 근거다. 계정 삭제/보관기간/관리자 대시보드/원본 백업은 후속 기능으로 이슈화한다. 교육용 체크 통과는 전체 보안 감사 완료를 의미하지 않는다.

목차와 처음으로 ↑
EXPORT / SECURITY

CSV의 범위·형식·수식 위험·취소

버튼은 현재 검색/필터와 무관하게 로그인한 본인 과제 전체를 새로 조회한다. 현재 화면 목록은 최대1,000건, 내보내기는 최대10,000건이다. 상한 초과·페이지 오류·건수 변경·계정 전환에는 부분 CSV를 생성하지 않는다.

항목예제 동작
인코딩·형식UTF-8 BOM·CRLF·4열·모든 셀 인용·내부 따옴표 두 번
표시 데이터과제명·과목·마감일·완료만, uid/이메일/토큰 제외
위험 입력공백 뒤 = + - @·전각 변형 또는 탭/개행 시작을 검사해 앞에 탭 추가
조회uid 필터+RLS,500행씩 created_at/id순·exact count·중복 검사
인증getUser uid·currentLevel aal2·epoch/AbortController
취소·오류다운로드0회·상태 안내·중복 실행 방지
빈 목록헤더만 있는 CSV
동시 편집조회 중 다른 탭의 편집 중지. 단일 트랜잭션 스냅샷 아님

수식 위험 변환은 사람의 스프레드시트 열람을 위한 처리다. 탭이 실제 셀 값에 포함되므로 원본 복원/재가져오기용 백업으로 쓰지 않는다. OWASP는 인용/이스케이프만으로 모든 스프레드시트 및 재저장을 보장할 수 없다고 설명한다. 교실의 Excel/Sheets에서 텍스트 가져오기로 실제 열람을 확인한다. DB 원본은 그대로 둔다.

CSV 생성·다운로드·인증/취소 전체 코드

// 사람의 스프레드시트 열람용입니다. 원본 복원/재가져오기 형식이 아닙니다.
function cell(value, userInput = false) {
  let text = String(value ?? '');
  // 인용만으로 수식이 막히지 않습니다. 전각·선행 공백/제어문자도 검사합니다.
  if (userInput && (/^\s*[=+\-@=+-@]/u.test(text) || /^[\t\r\n]/u.test(text))) text = '\t' + text;
  return '"' + text.replaceAll('"', '""') + '"';
}

export function toTaskCsv(tasks) {
  const rows = [['과제명', '과목', '마감일', '완료'].map(v => cell(v)).join(',')];
  for (const task of tasks) rows.push([
    cell(task.title, true), cell(task.subject, true), cell(task.due), cell(task.done ? '완료' : '미완료')
  ].join(','));
  return '\uFEFF' + rows.join('\r\n') + '\r\n';
}

export function downloadTaskCsv(csv) {
  const url = URL.createObjectURL(new Blob([csv], {type:'text/csv;charset=utf-8'}));
  const link = document.createElement('a');
  link.href = url;
  link.download = `my-tasks-${new Date().toISOString().replaceAll(':','-').slice(0,19)}Z.csv`;
  document.body.append(link);
  try { link.click(); }
  finally { link.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000); }
}

export function createCsvExporter(client, store, download = downloadTaskCsv) {
  let active = null;
  return {
    cancel() { active?.abort(); },
    async run(userId, isCurrent, onProgress) {
      if (active) throw {code:'EXPORT_BUSY'};
      const controller = new AbortController();
      active = controller;
      function check() {
        controller.signal.throwIfAborted();
        if (!isCurrent()) throw {code:'EXPORT_CANCELLED'};
      }
      try {
        check();
        const {data, error} = await client.auth.getUser();
        check();
        if (error || data.user?.id !== userId) throw {code:'EXPORT_AUTH'};
        const aal = await client.auth.mfa.getAuthenticatorAssuranceLevel();
        check();
        if (aal.error || aal.data.currentLevel !== 'aal2') throw {code:'EXPORT_AUTH'};
        const tasks = await store.listForExport(userId, controller.signal, onProgress);
        check();
        const csv = toTaskCsv(tasks);
        check();
        download(csv);
        return tasks.length;
      } finally {
        if (active === controller) active = null;
      }
    }
  };
}

DB 페이지 조회와 실패 처리

const FIELDS = 'id,user_id,title,subject,due,done,created_at';
function item(row) {
  return {id:row.id,title:row.title,subject:row.subject,due:row.due,done:row.done,createdAt:Date.parse(row.created_at)};
}
function requireResult(result) { if(result.error)throw result.error;return result.data; }
export function createTaskStore(client) {
  return {
    async listForExport(userId, signal, onProgress) {
      const pageSize=500, maxRows=10000, result=[], seen=new Set();
      let expected=null;
      for(let offset=0;;offset+=pageSize){
        signal.throwIfAborted();
        const response=await client.from('tasks')
          .select('id,title,subject,due,done,created_at',{count:'exact'})
          .eq('user_id',userId).order('created_at').order('id')
          .range(offset,offset+pageSize-1).abortSignal(signal);
        signal.throwIfAborted();
        const rows=requireResult(response);
        if(expected===null)expected=response.count;
        if(!Number.isSafeInteger(expected)||expected<0||response.count!==expected)throw {code:'EXPORT_CHANGED'};
        if(expected>maxRows)throw {code:'EXPORT_LIMIT'};
        if(rows.length!==Math.min(pageSize,expected-offset))throw {code:'EXPORT_CHANGED'};
        for(const row of rows){if(seen.has(row.id))throw {code:'EXPORT_CHANGED'};seen.add(row.id);result.push(row);}
        onProgress?.(result.length,expected);
        if(result.length===expected)return result;
      }
    },
    async list(userId) {
      const rows=requireResult(await client.from('tasks').select(FIELDS).eq('user_id',userId).order('created_at').limit(1000));
      return rows.map(item);
    },
    async add(userId,values) {
      return item(requireResult(await client.from('tasks').insert({user_id:userId,title:values.title,subject:values.subject,due:values.due}).select(FIELDS).single()));
    },
    async addMany(userId,values) {
      const rows=requireResult(await client.from('tasks').insert(values.map(v=>({user_id:userId,title:v.title,subject:v.subject,due:v.due}))).select(FIELDS));
      return rows.map(item);
    },
    async update(userId,id,values) {
      const allowed={};
      for(const key of ['title','subject','due','done'])if(key in values)allowed[key]=values[key];
      return item(requireResult(await client.from('tasks').update(allowed).eq('id',id).eq('user_id',userId).select(FIELDS).single()));
    },
    async remove(userId,id) {
      const rows=requireResult(await client.from('tasks').delete().eq('id',id).eq('user_id',userId).select('id'));
      if(rows.length!==1)throw {code:'NO_ROW'};
    },
    async restore(userId,task) {
      return item(requireResult(await client.from('tasks').insert({id:task.id,user_id:userId,title:task.title,subject:task.subject,due:task.due,done:task.done,created_at:new Date(task.createdAt).toISOString()}).select(FIELDS).single()));
    }
  };
}
목차와 처음으로 ↑
A / B / ANONYMOUS / AAL

계정별 권한과 실제 API 검사

세션조회추가수정/삭제CSV
익명권한거절거절거절UI/인증거절
A본인 AAL10행거절0행/거절인증거절
A본인 AAL2허용허용허용A전체만
B AAL2→A행0행소유자위조거절0행/거절B전체만·A없음

아래 코드는 가상 계정·본인 로컬 Vite 개발 서버에서만 DevTools Console로 실행한다. B는 별도 브라우저 프로필에서 로그인·MFA를 마친다. targetId/targetOwner는 A가 만든 가상 과제의 UUID와 A의 uid로 바꾼다. 배포 번들에는 /src 경로가 없으므로 운영 Console에 그대로 붙이지 않는다. JWT·브라우저 세션·전체 client 객체를 출력하지 않는다.

const {supabase: qa} = await import('/src/supabase-client.js');
const targetId = 'A의-가상과제-UUID';
const targetOwner = 'A의-user-UUID';
const read = await qa.from('tasks').select('id').eq('id',targetId);
console.log('조회행',read.data?.length,'오류코드',read.error?.code);
const change = await qa.from('tasks').update({done:true}).eq('id',targetId).select('id');
console.log('수정행',change.data?.length,'오류코드',change.error?.code);
const remove = await qa.from('tasks').delete().eq('id',targetId).select('id');
console.log('삭제행',remove.data?.length,'오류코드',remove.error?.code);
const forged = await qa.from('tasks').insert({user_id:targetOwner,title:'위조 검사',due:'2026-11-11'});
console.log('소유자위조추가 거절',!!forged.error);
const mine = await qa.from('tasks').select('id').limit(1);
if(mine.data?.[0]) {
  const transfer = await qa.from('tasks').update({user_id:targetOwner}).eq('id',mine.data[0].id);
  console.log('소유자변경 거절',!!transfer.error);
}
// A 프로필에서 원본의 존재·done·소유자가 바뀌지 않았는지 새로고침해 확인

UUID 자리표시자를 그대로 두면 단순 형식 오류만 검사하게 된다. 실제 가상 UUID를 넣고 표와 대조한다. 익명은 로그아웃한 개발 서버에서 동일 SDK로 SELECT/INSERT를 시도한다. AAL1은 등록된 계정의 재로그인 직후, AAL2는 인증앱 코드 확인 후 검사한다. QR/코드는 보고서에 넣지 않는다. 소유자 정책4개 AND AS RESTRICTIVE AAL2를 함께 유지한다.

목차와 처음으로 ↑
TWO ENVIRONMENTS / RELEASE

staging→main 최종 배포 실행표

환경브랜치Vercel 범위DB앱 주소
로컬feature/csv-releaseDevelopment / .env.local수업 staging DBhttp://127.0.0.1:5173/
stagingstagingPreview (필요 시 branch override)별도 수업 staging DB실제 고정/현재 Preview URL
ProductionmainProduction별도 수업 production DB본인 운영 URL
  1. GitHub에서 기존 main을 기준으로 staging 브랜치를 준비한다. staging area는 git add로 만드는 커밋 대기 영역이며 브랜치와 다르다.
  2. Vercel Settings에서 Production Branch=main을 확인한다. staging push는 보통 Preview다. Preview 두 VITE_ 값은 staging DB, Production 두 값은 별도 production DB로 설정한다. 다른 Preview도 쓰면 staging branch override를 확인한다.
  3. 본인 수업용 새 DB를 sql/inspect.sql로 먼저 확인한 뒤 setup.sql→mfa-required.sql을 검토·적용한다. 이미 존재하면 중복 실행하지 말고 변경 migration을 검토한다. Auth 가입·이메일/비밀번호·TOTP 설정은 DB SQL과 별도다.
  4. staging/production Supabase의 Site URL·Redirect URLs, 제공자마다 표시하는 Supabase callback, SMTP/Resend 발신·이메일 확인/복구, Naver 고정 서버 어댑터를 환경별 점검한다. 비밀은 각 서비스 설정에 보관한다.
  5. feature/csv-release→staging PR을 검토·병합한다. Preview READY·commit·URL·DB 이름을 대조하고 로그인→MFA→과제 추가/완료/재조회→CSV→B분리 검사를 한다.
  6. 모두 통과하면 staging→main PR을 검토·병합한다. main은 Production 환경으로 새 빌드한다. 운영 URL의 동일 smoke 검사를 반복하고 기록한다.
  7. 실패/미확인이 있으면 출시 완료 표시를 멈춘다. 필요하면 이전 정상 앱 배포를 복원하고, DB 스키마/데이터 복구는 별도 검토한다.

이 수업은 main의 Git 자동 배포를 사용한다. Preview 검증만으로 Production의 환경·메일·SNS가 검증되지는 않는다. Vercel에서 Preview를 promote하는 별도 기능도 Production 설정으로 다시 빌드될 수 있으므로 Preview URL을 그대로 운영 검증 결과로 적지 않는다. Vercel 배포는 Supabase SQL을 자동 적용하지 않는다.

목차와 처음으로 ↑
RELEASE EVIDENCE

릴리스 기록과 복구 담당자

# 과제 체크 앱 최종 릴리스 기록
검증 일시/확인자:
저장소 / feature PR → staging:
staging commit / Preview URL / Supabase 프로젝트 이름:
staging 빌드 상태 / 실제 검사 근거:
staging PR → main:
main commit / Production URL / Supabase 프로젝트 이름:
Production 빌드 상태 / 실제 검사 근거:
DB SQL 검토·적용 순서/기록 (키/연결문자열 제외):
이메일·각 SNS·MFA 실제/모의/미확인:
실패·미확인 / 출시 판단:
이전 정상 앱 배포 / 복원 담당자:
DB 변경 유무 / 별도 DB 복구 절차:
다음 이슈 / 동료 피드백:

ZIP의 environments.csv와 release-record.md에 실제 값을 기록하되 키·비밀번호·연결 문자열은 넣지 않는다. URL/commit/프로젝트 이름과 검사 시각이면 근거를 대조할 수 있다.

목차와 처음으로 ↑
DEMO / HANDOFF

3분 발표 원고·100점 평가·인수인계

시간내용보여줄 근거
0:00~0:30문제·대상·웹 선택누구의 어떤 과제 관리 문제를 해결했나
0:30~1:30실제 기능가상 과제 추가/완료·검색·CSV 확인
1:30~2:15권한·추가 인증A/B 검사 근거와 AAL1/AAL2 구분
2:15~2:45배포 근거staging/Production URL·commit·DB 환경
2:45~3:00한계·다음 개선미확인 항목과 동료 피드백 이슈
평가배점기준
문제·사용자15실제 대상·문제·웹/모바일 선택 이유
핵심 동작25CRUD·검색·CSV 실제 시연
권한·보안 근거25A/B·익명·AAL·실패검증/미확인 구분
배포·재현성20두 환경·PR·commit·README·복구 기록
발표·개선153분 준수·비밀없는가상자료·다음 이슈

발표3분+피드백2분을6명 소그룹으로30분 운영한다. 발표용 MFA는 사전 완료하고 비밀 QR/OTP를 화면 공유하지 않는다. CSV도 본인 가상 과제만 포함한다. 개선점1개는 peer-feedback.md와 GitHub 이슈에 남긴다.

최종 제출 묶음

  • main 저장소·PR2개·Preview/Production URL과 commit
  • README: 실행·환경 변수 이름·SQL 적용 순서·검증·CSV 한계·배포/복구
  • checks.csv·environments.csv·release-record.md
  • 3분 발표 원고·가상 CSV·동료 피드백·후속 이슈1개
  • 실제/모의/미확인 목록과 다음 확인 담당자
목차와 처음으로 ↑
FINAL CHECK

최종 통합 검사표

검사행동기대 결과
CSV범위A로그인/AAL2·검색1건·내보내기A전체 DB과제, B행/메일/uid/token없음
CSV형식한글·쉼표·따옴표·줄바꿈UTF-8 BOM·4열·인용·행경계보존
수식위험 =1+2·+1·-1·@SUM·전각·탭/개행자유입력앞탭변환·DB원본유지·스프레드시트텍스트열람
빈목록0건 계정 내보내기4열 헤더만 생성
페이지가상1,203건과10,001건1,203건 누락/중복없음·상한초과파일없음
중간실패2페이지 오류/건수 변경오류 안내·부분파일0회
계정전환A조회중로그아웃/B전환A다운로드0회·B화면A정보없음
익명Data API SELECT/INSERT권한 거절·DB불변
본인AAL1재로그인 후 CRUD/CSV조회0행·쓰기거절/0행·CSV거절
본인AAL2MFA후 CRUD/CSV본인만 성공·재조회유지
타인AAL2B가 A id조회/수정/삭제0행 또는 거절·A원본불변
소유자위조B의user_id=A추가/변경거절·DB불변
비밀tracked파일·diff·번들검사secret/service_role/SMTP/OAuth비밀없음
stagingPreview URL·commit·DB·기능검사staging DB·로그인/MFA/CRUD/CSV/A-B확인
mainProduction URL·commit·DB·재검사production DB·로그인/MFA/CRUD/CSV/A-B확인
발표3분 시연·동료 피드백·이슈가상자료·근거/미확인분리·다음개선1개

Codex 최종 검증 요청

내 과제 앱의 최종 검증을 실행 가능한 범위에서 진행하고 checks.csv에 근거를 남겨줘.
가상 A/B 계정, 익명, AAL1과 AAL2로 직접 Data API를 검사해. 본인 CRUD 허용, 타인 조회/수정/삭제의 0행 또는 거절과 원본 불변, user_id 위조 추가/변경 거절을 확인해. UI가 숨겨졌다는 이유로 권한 검사를 통과시키지 마.
CSV가 본인 전체 DB 과제만 새 조회하는지, 검색 결과와 무관한지, 한글/인용/CRLF/위험 입력/빈 목록/1,203행을 처리하는지 검사해. AAL1 내보내기 거절, 페이지 실패와 계정 전환에서 다운로드 0회를 확인해.
비밀은 값 대신 존재 여부와 변수 이름만 보고해. git diff --cached, .gitignore, 번들에 secret/service_role/SMTP/OAuth secret이 없는지 점검해. publishable key는 공개 식별값이며 RLS를 대신하지 않음을 명시해.
빌드·PR·배포상태와 실제 URL 기능 검사를 분리해. 로컬 통과, 모의 통과, staging 실제, Production 실제, 미실행을 구분해 결과/시각/commit/검증자를 기록해. 실패·미확인 항목이 남으면 출시 완료라고 쓰지 마.

통과/실패/미확인과 실제 결과·시각·확인자를 CSV 양식에 적는다. 테스트 계정/키/QR/코드/토큰을 근거로 첨부하지 않는다.

목차와 처음으로 ↑
TROUBLESHOOTING / SCOPE

문제 해결과 예제의 검증 범위

증상확인조치
CSV 한글 깨짐BOM·문자 인코딩·가져오기 옵션UTF-8 텍스트로 가져오기
열/행이 나뉨큰따옴표·이스케이프·CRLF줄바꿈 있는 셀을 파서/스프레드시트로 확인
검색과 건수 다름내보내기 범위 설명화면 검색과 관계없이 본인 전체 재조회
CSV 수식처럼 보임위험문자 변환·텍스트 가져오기원본 DB를 수정하지 말고 변환/열람 검증
일부 행만 내보냄페이지·exact count·상한·동시편집편집 중지 후 재시도·부분파일 금지
타인 요청200반환 배열·affected rows·A원본0행과 원본 불변까지 기록
AAL1 접근됨restrictive정책·실제 JWT 강도권한SQL과 실제 API 재검사
staging이 운영DB를 봄Preview 환경변수·branch override올바른 DB로 수정 후 새 빌드
main만 로그인 실패Production Site/Redirect·SNS/SMTP새 운영 URL에서 별도 검증
Naver Preview 실패서버 UserInfo URL·인증 보호·callback준비된 고정 수업 어댑터에서 검사
롤백했는데 DB는 그대로앱 배포와 DB 변경 분리검토된 별도 DB 복구 절차 사용

완성 소스는 격리된 로컬 Supabase의 실제 TOTP/AAL/RLS와1,203행 CSV 페이지 조회, 타인 제외·소유자 위조·계정 전환·실패 처리를 검사한다. ZIP 클린 빌드와 HTML 발표·교재를 검수한다. 학생의 실제 cloud staging/Production DB 연결, 이메일 발송, 세 SNS 제공자 승인/동의 왕복, 실제 교실 스프레드시트 열람은 각 수업 계정에서 최종 확인해야 한다. 강의 사이트 게시와 학생 앱의 두 환경 릴리스는 별개다.

근거와 참고 자료
목차와 처음으로 ↑

프롬프트 직접 복사

아래 전체 문장을 선택해서 복사하세요.