OFFLINE / DAY 5
5일차 · 4교시 본문 교재
이 파일에는 해당 일차의 교시·실습·프롬프트·강사 설명만 담았습니다. 실습 교재에는 학생용 단계별 절차가 있습니다.
이 폴더 안에서 발표·교재·코드·심화 원문을 오프라인으로 이동할 수 있습니다.
OFFLINE / DAY 5
이 파일에는 해당 일차의 교시·실습·프롬프트·강사 설명만 담았습니다. 실습 교재에는 학생용 단계별 절차가 있습니다.
| 기술 | 종류 | 역할 |
|---|---|---|
| HTML·CSS | 마크업·스타일 언어 | 화면 구조와 표현 |
| JavaScript·TypeScript | 프로그래밍 언어 | 화면·서버의 동작 |
| React·Next.js | UI 라이브러리·웹 프레임워크 | 컴포넌트·페이지·서버 경로 |
| Node.js | 서버 실행 환경 | TypeScript/JavaScript 서버 실행 |
| PostgreSQL·SQL | 관계형 DB·질의 언어 | 저장·조회·무결성 |
| Supabase | 백엔드 플랫폼 | DB·Auth·Storage·API |
| Docker·Vercel | 컨테이너 도구·배포 플랫폼 | 로컬 스택·공개 앱 |
세 프로젝트의 주 DB는 Supabase PostgreSQL이다. MySQL·SQLite는 관계형 DB, MongoDB는 문서형 DB의 비교 예시다. uc-anchor의 선택형 Python PDF 서비스는 Next.js·Vercel 기본 배포와 분리해 설명한다. 현재 운영 전체 기능을 재검증한 표는 아니다.
목차와 처음으로 ↑본인 과제 내보내기, 계정별 보안, staging→main 배포와3분 발표
과제 앱 본인 과제 CSV 내보내기를 같은 앱에서 실행하고 근거를 남긴다.
산출물: CSV 완성 앱·형식/경계 검사
| 번호 | 제목 | 진행 내용 |
|---|---|---|
| 160 | 내 과제를 CSV로 가져가기 | 아래 단계별 실습을 진행한다. |
| 161 | CSV 형식과 수식 위험은 별개 | 쉼표·큰따옴표·줄바꿈이 있어도4열을 유지 / 위험한 자유 입력 앞 탭 · DB 원본은 유지 / 스프레드시트 열람용 · 원본 복원 백업 아님 / 페이지 실패·상한 초과는 부분 파일을 만들지 않음 |
| 162 | 과제 앱 본인 과제 CSV 내보내기 | 과제 앱 본인 과제 CSV 내보내기를 같은 앱에서 실행하고 근거를 남긴다. / 산출물: CSV 완성 앱·형식/경계 검사 / 목표5·복원8·구현22·경계검증11·정리4분 |
| 163 | 핵심 개념 | 본인 전체 DB 재조회·페이지 처리 / CSV 인용·BOM·수식 위험 변환 / 다운로드 취소·상한·동시 편집 한계 |
| 164 | 내보내기는 다운로드 뒤에도 검증한다 | 현재 검색 배열 대신 본인 전체 과제를 DB에서 새 조회한다. / 한글·인용·줄바꿈·수식 위험 입력과 열 범위를 검사한다. / 빈 목록·페이지 오류·계정 전환에서 파일 생성 결과를 확인한다. / 스프레드시트 열람용 CSV와 원본 복원 백업의 용도를 구분한다. |
| 165 | kysong · 이번 실습에 적용할 판단 | 기록: Excel·HWPX 내보내기를 업무 결과물로 확장했다. / 판단: 파일 생성 성공 이후 실제 열기·수정 가능 여부도 검사했다. / 적용: 과제 CSV도 내려받아 원본 건수·열·문자와 비교한다. |
| 166 | 실습 절차 | 아래 단계별 실습을 진행한다. |
| 167 | Codex에 입력할 요청 | 아래 전체 구현 요청을 사용한다. |
| 168 | 완료 확인과 오류 대응 | 미커밋 변경을 먼저 정리하고 잘못된 저장소에서 실행하지 않는다. 환경표에 URL·브랜치·DB 이름만 적는다. 두 환경은 같은 DB를 가리키지 않는다. / 한글·쉼표·큰따옴표·줄바꿈·=1+2·전각=1+2와 빈 목록을 검사한다. 위험한 자유 입력 앞의 탭은 열람용 변환이며 DB 원본은 유지된다. 토큰·이메일·uid가 CSV에 없다. |
M01~M08과 E01~E08을 사용한다. CSV·계정별 권한·staging→main·발표를 같은 앱에서 완주한다.
5일차 단계·검사표·배포·발표·강사 설명 ↗ · 시작·완성·제출 양식 ZIP
해당 교시의 설명을 대체하거나 실습 뒤 선택한다. 별도 수업시간을 추가하지 않는다.
기존 파일 업무 사례를5일차 CSV 검증 기준으로 가져온다. 원본 보존과 열람용 변환의 차이를 실제 예시로 설명한다.
1교시50분은 목표5·복원8·CSV구현22·경계검증11·정리4다. 두 Supabase 프로젝트/키 준비가 지연되지 않게 강사가 별도 수업 프로젝트를 미리 확보한다. 계정 무료 프로젝트 한도를 수업 전에 확인하고 필요하면 강사 제공 환경으로 진행한다. 실제 프로젝트의 운영 DB를 재사용하지 않는다. CSV 인용은 형식 보존, 수식 위험 변환은 별도 보안 처리다. 탭 접두어는 Excel 열람의 위험을 줄이지만 모든 스프레드시트와 재저장에 대한 보장은 아니다. 코드 검증과 실제 교실 Excel/Sheets에서 텍스트 가져오기 검증을 나눠 기록한다. UI 입력이 trim하는 경우 줄바꿈/선행 제어문자 검사는 코드 테스트로 보충한다. E01~E08과 M01~M08의 교재·시작/완성 코드를 사용한다.
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를 끄지 마.내 과제 앱의 최종 검증을 실행 가능한 범위에서 진행하고 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에 화면 배열을 그대로 쓰면 어떤 문제가 생기는가?
검색·목록 한도·오래된 계정 데이터가 섞일 수 있어 본인 전체 DB를 새로 조회하고 세션을 검증한다.
과제 앱 계정별 권한·보안 최종 검증를 같은 앱에서 실행하고 근거를 남긴다.
산출물: A/B·익명·AAL·실패 검사표
| 번호 | 제목 | 진행 내용 |
|---|---|---|
| 169 | 계정이 바뀌어도 안전한가? | 익명·AAL1 차단 / 본인 AAL2만 허용 / B의 A행 조회·수정·삭제는0행 또는 거절 / user_id 위조 추가·변경 거절을 확인 / A조회 중 계정 전환 → A CSV 다운로드0회 |
| 170 | Codex: 성공과 거절을 함께 검증 | 아래 전체 구현 요청을 사용한다. |
| 171 | 과제 앱 계정별 권한·보안 최종 검증 | 과제 앱 계정별 권한·보안 최종 검증를 같은 앱에서 실행하고 근거를 남긴다. / 산출물: A/B·익명·AAL·실패 검사표 / 원리7·A/B/API20·실패/취소10·비밀검사9·정리4분 |
| 172 | 핵심 개념 | UI 표시와 DB 권한 구분 / 소유자 RLS AND AAL2 / 실패·미확인·비밀 노출 검사 |
| 173 | 권한·비밀·실패를 증거로 검증 | 익명·AAL1·AAL2·A/B의 직접 API 요청을 대조한다. / HTTP200도0행일 수 있어 영향 행과 원본 불변을 함께 확인한다. / UI 역할·user_metadata를 권한 근거로 사용하지 않는다. / 로컬·모의·실제·미확인을 구분하고 비밀 없는 근거를 남긴다. |
| 174 | kysong · 이번 실습에 적용할 판단 | 기록: 여러 임시저장을 복구할 때 원본 보존과 정합성을 보완했다. / 교훈: 다른 사용자의 기록을 섞어 복구하거나 빈값을 AI가 채우면 안 된다. / 적용: 계정 전환 중 늦은 응답·CSV 취소까지 보안 검사에 포함한다. |
| 175 | 실습 절차 | 아래 단계별 실습을 진행한다. |
| 176 | Codex에 입력할 요청 | 아래 전체 구현 요청을 사용한다. |
| 177 | 완료 확인과 오류 대응 | AAL2는 관리자 권한이 아니다. B의 A행 조회/수정/삭제는0행 또는 거절이며 A원본 불변. 익명은 거절. AAL1은 본인 조회0행·쓰기거절/0행이며 내보내기 불가. 소유권 위조 추가/변경은 거절. / 부분 CSV·이전 계정 CSV·중복 다운로드0회. 예제 상한10,000건을 넘으면 명시 오류. 위 git ls-files 결과는 비어야 한다. 공개 URL/key 외 secret·service_role·SMTP·OAuth 비밀이 소스/번들에 없다. |
M01~M08과 E01~E08을 사용한다. CSV·계정별 권한·staging→main·발표를 같은 앱에서 완주한다.
5일차 단계·검사표·배포·발표·강사 설명 ↗ · 시작·완성·제출 양식 ZIP
해당 교시의 설명을 대체하거나 실습 뒤 선택한다. 별도 수업시간을 추가하지 않는다.
보이는 버튼이나 성공 메시지보다 실제 권한 경계와 원본 불변을 평가한다. 미확인을 통과로 바꾸지 않는다.
2교시50분은 원리7·A/B/API20·실패/취소10·비밀검사9·정리4다. 표의 기대 결과를 읽는 것과 실제 요청 성공/거절을 구분한다. HTTP200의 빈 배열도 RLS 차단일 수 있으므로 반환 행과 A원본을 함께 확인한다. 이메일이 같음/화면의 관리자 표기/user_metadata.role을 권한 근거로 쓰지 않는다. 1,203건은 기능 테스트가 끝나면 가상 수업 DB에서만 정리한다. 화면 목록의1,000건 한도와 CSV10,000건 한도를 구분한다. 다른 탭의 동시 편집은 잠시 멈춘다. 페이지 조회는 단일 트랜잭션 스냅샷이 아니며 동일 건수의 동시 편집까지 완전 감지하는 구현이 아니다. 실패를 RLS 해제로 해결하지 않는다. E01~E08과 M01~M08의 교재·시작/완성 코드를 사용한다.
내 과제 앱의 최종 검증을 실행 가능한 범위에서 진행하고 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/검증자를 기록해. 실패·미확인 항목이 남으면 출시 완료라고 쓰지 마.내 과제 앱의 최종 검증을 실행 가능한 범위에서 진행하고 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/검증자를 기록해. 실패·미확인 항목이 남으면 출시 완료라고 쓰지 마.HTTP200이면 타인 권한 검사가 실패한 것인가?
반환0행일 수 있다. 영향 행과 원본 불변까지 확인한다.
과제 앱 staging→main 최종 배포를 같은 앱에서 실행하고 근거를 남긴다.
산출물: 두 환경 URL·commit·PR·릴리스 기록
| 번호 | 제목 | 진행 내용 |
|---|---|---|
| 178 | staging은 Preview, main은 Production | 아래 단계별 실습을 진행한다. |
| 179 | 검증 → PR → 최종 URL 재검사 | staging PR 검토·병합 → Preview 실제 검사 / staging→main PR → Production 새 빌드 / 운영 URL의 MFA·CRUD·CSV·A/B 다시 검사 / 앱 롤백과 DB 복구는 별도 절차다 |
| 180 | 과제 앱 staging→main 최종 배포 | 과제 앱 staging→main 최종 배포를 같은 앱에서 실행하고 근거를 남긴다. / 산출물: 두 환경 URL·commit·PR·릴리스 기록 / 환경8·PR/Preview15·검증10·main/운영13·정리4분 |
| 181 | 핵심 개념 | staging area와 staging 브랜치 / Preview/Production·두 DB·환경별 Auth / PR→재빌드→실제 URL 재검사 |
| 182 | staging→main: Git 기록과 운영 설정 연결 | staging 브랜치의 Preview와 main의 Production을 구분한다. / 두 환경의 URL·DB·설정 이름·검증 commit을 기록한다. / PR 검토→병합→새 빌드→실제 URL 재검사를 순서대로 확인한다. / READY·CANCELED·SKIPPED·ERROR는 기능 검증 결과와 다르다. |
| 183 | uc-anchor · 이번 실습에 적용할 판단 | 기록: PR #720·#722의 로컬 Git 병합 이력과 Thomas dot의 staging/Preview 논의를 확인했다. / 범위: 과거 배포 상태는 당시 관찰이며 이번 운영 재검증 결과가 아니다. / 적용: 학생 앱의 검증 commit·배포 commit·URL·DB 대상을 직접 남긴다. |
| 184 | 실습 절차 | 아래 단계별 실습을 진행한다. |
| 185 | Codex에 입력할 요청 | 아래 전체 구현 요청을 사용한다. |
| 186 | 완료 확인과 오류 대응 | 환경 변수 변경 뒤 새 빌드를 만든다. 프런트엔드 URL/key 외 비밀값은 VITE_로 만들지 않는다. DB migration은 Vercel 코드 배포와 별도다. 기존 정책이 있으면 SQL을 무작정 재실행하지 않는다. / PR별 base/head·commit·실제 URL·DB 이름·검증자·결과가 남는다. Preview 통과와 Production 재빌드 통과를 별도로 기록한다. 실패하면 배포 완료 표시를 멈추고 원인을 수정한다. |
M01~M08과 E01~E08을 사용한다. CSV·계정별 권한·staging→main·발표를 같은 앱에서 완주한다.
5일차 단계·검사표·배포·발표·강사 설명 ↗ · 시작·완성·제출 양식 ZIP
해당 교시의 설명을 대체하거나 실습 뒤 선택한다. 별도 수업시간을 추가하지 않는다.
과거 사례의 성공을 학생 배포의 성공으로 대신하지 않는다. main은 Production 환경의 새 빌드이며 DB 복구는 앱 롤백과 별도다.
3교시50분은 환경8·PR/Preview15·검증10·main/운영13·정리4다. Git의 staging area는 커밋 대기 영역, staging 브랜치는 팀이 정한 이름이다. 브랜치 이름만으로 Vercel Production이 되지 않는다. 새 프로젝트의 Auth 사용자는 독립적이므로 같은 사람이 두 DB에서 다른 uid를 가질 수 있다. 운영 데이터 복제 없이 가상 계정으로 검증한다. 수업용 staging 브랜치는 시작 전에 main에서 생성해 원격에 둔다. main 병합은 Production 환경으로 새 빌드하므로 preview와 동일 실행물/설정이라고 단정하지 않는다. 배포 롤백은 앱 버전 복원이며 이미 저장한 DB 데이터나 스키마를 자동 복원하지 않는다. 잘못된 데이터 변경은 별도 검토된 DB 복구 절차가 필요하다. E01~E08과 M01~M08의 교재·시작/완성 코드를 사용한다.
현재 과제 앱을 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·보호 해제·비밀 커밋 없이 진행해줘.내 과제 앱의 최종 검증을 실행 가능한 범위에서 진행하고 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/검증자를 기록해. 실패·미확인 항목이 남으면 출시 완료라고 쓰지 마.staging Preview 통과 후 main에서도 다시 검사하는 이유는?
Production 설정으로 새 빌드되므로 DB·Auth·공개 URL을 다시 확인해야 한다.
과제 앱 3분 발표와 인수인계를 같은 앱에서 실행하고 근거를 남긴다.
산출물: 3분 원고·README·피드백 이슈
| 번호 | 제목 | 진행 내용 |
|---|---|---|
| 187 | 3분 안에 앱의 가치를 보여주기 | 아래 단계별 실습을 진행한다. |
| 188 | 5일의 결과를 인수인계하기 | 1일:무료생성 /2일:Git개선 /3일:DB·Auth / 4일:SNS·MFA /5일:CSV·검증·최종배포 / README·검사표·PR·URL·발표 원고 제출 / 실제·모의·미확인을 구분하고 다음 이슈1개 |
| 189 | 과제 앱 3분 발표와 인수인계 | 과제 앱 3분 발표와 인수인계를 같은 앱에서 실행하고 근거를 남긴다. / 산출물: 3분 원고·README·피드백 이슈 / 준비8·발표/짝피드백30·이슈/인수인계8·정리4분 |
| 190 | 핵심 개념 | 문제·대상·가치와 3분 시연 / 검증 근거와 실제/모의/미확인 / README·릴리스·복구·다음 이슈 |
| 191 | 만든 과정과 다음 운영을 인수인계 | 문제·사용자→실제 동작→검증 근거→배포→한계를3분으로 보여준다. / README에 실행·설정 이름·SQL 순서·검사·복구 담당자를 적는다. / 동료 피드백1개를 다음 이슈로 남기고 미완료 항목을 명시한다. / 발표 자료·CSV·캡처에는 가상 데이터만 사용한다. |
| 192 | u-livet · 이번 실습에 적용할 판단 | 기록: 로컬 실험·운영 선택·SNS·MFA 후속 보완이 함께 남아 있다. / 교훈: 처음 만든 버전과 후속 수정, 실제/모의/미확인을 나눠 설명한다. / 적용: 과제 앱의5일 구축 과정과 다음 운영 작업을 재현 가능하게 전달한다. |
| 193 | 실습 절차 | 아래 단계별 실습을 진행한다. |
| 194 | Codex에 입력할 요청 | 아래 전체 구현 요청을 사용한다. |
| 195 | 완료 확인과 오류 대응 | 저장소/운영 링크·화면 시연·권한 근거·staging/Production commit·다음 개선1개가 있다. QR·OTP·토큰·실제 학생 정보가 발표 화면/CSV/캡처에 없다. / 본인이 하지 않은 실SNS·운영 검증을 통과로 표시하지 않는다. 발표용 CSV도 본인 가상 자료만 제출한다. 미커밋 작업·열린 PR·실패 검사·복구 담당자가 분명하다. |
M01~M08과 E01~E08을 사용한다. CSV·계정별 권한·staging→main·발표를 같은 앱에서 완주한다.
5일차 단계·검사표·배포·발표·강사 설명 ↗ · 시작·완성·제출 양식 ZIP
해당 교시의 설명을 대체하거나 실습 뒤 선택한다. 별도 수업시간을 추가하지 않는다.
전체 프로젝트 연표와 자세한 기술 결정은 별도 사례 파일로 옮겼다. 발표에서는 이번 과제 앱과 관련된 판단 한 가지를 근거로 설명한다.
4교시50분은 준비8·발표/짝피드백30·이슈/인수인계8·정리4다. 3분 발표+2분 피드백을6명 소그룹에서 병렬 진행하고 대표 사례만 전체 공유한다. 대규모 수업에서 전원이 전원 앞 발표하면 시간을 넘는다. “AI가 만들었다”보다 문제와 검증 근거를 평가한다. 성공 기준은 공개 링크 하나가 아니라 재현 가능한 실행·권한·배포 근거다. 계정 삭제/보관기간/관리자 대시보드/원본 백업은 후속 기능으로 이슈화한다. 교육용 체크 통과는 전체 보안 감사 완료를 의미하지 않는다. E01~E08과 M01~M08의 교재·시작/완성 코드를 사용한다.
완성한 과제 체크 앱의 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점 평가표도 포함해줘.내 과제 앱의 최종 검증을 실행 가능한 범위에서 진행하고 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/검증자를 기록해. 실패·미확인 항목이 남으면 출시 완료라고 쓰지 마.미확인 기능을 발표에서 어떻게 설명해야 하는가?
로컬·모의·실제·미확인을 구분하고 다음 확인 계획을 제시한다.
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분으로 줄이고 나머지는 구현·검증·발표에 쓴다.
SNS·MFA·DB
본인 전체 재조회
실제 API·실패
staging→main
근거·인수인계
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를 끄지 마.현재 과제 앱을 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분 시연과 인수인계 자료를 만들어줘.
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점 평가표도 포함해줘.내 과제 앱의 최종 검증을 실행 가능한 범위에서 진행하고 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/검증자를 기록해. 실패·미확인 항목이 남으면 출시 완료라고 쓰지 마.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를 재사용하지 않는다.
요청1을 Codex에 입력하고 변경 파일을 검토한다. solution의 csv-export.js·task-store.js·main.js·HTML/CSS와 비교한다. 추가 인증 후 내 과제 CSV 내보내기를 누른다. 검색 결과가 1건이어도 본인 전체 DB 과제가 들어가는지 확인한다. 내려받은 파일은 스프레드시트에서 텍스트로 가져오고 한글·열 이름·완료 상태를 확인한다.
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하는 경우 줄바꿈/선행 제어문자 검사는 코드 테스트로 보충한다.
가상 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을 권한 근거로 쓰지 않는다.
네트워크 차단 또는 모의 테스트로 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 해제로 해결하지 않는다.
환경표를 채운다. 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 어댑터를 점검한다.
현재 과제 앱을 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를 가질 수 있다. 운영 데이터 복제 없이 가상 계정으로 검증한다.
변경을 커밋·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-releasePR별 base/head·commit·실제 URL·DB 이름·검증자·결과가 남는다. Preview 통과와 Production 재빌드 통과를 별도로 기록한다. 실패하면 배포 완료 표시를 멈추고 원인을 수정한다.
수업용 staging 브랜치는 시작 전에 main에서 생성해 원격에 둔다. main 병합은 Production 환경으로 새 빌드하므로 preview와 동일 실행물/설정이라고 단정하지 않는다. 배포 롤백은 앱 버전 복원이며 이미 저장한 DB 데이터나 스키마를 자동 복원하지 않는다. 잘못된 데이터 변경은 별도 검토된 DB 복구 절차가 필요하다.
요청3으로 발표 원고를 작성하고 발표용 가상 과제3개와 CSV를 준비한다. 청중에게 비밀이 보이지 않게 MFA를 사전 완료한다. 문제/사용자→핵심 동작→권한 검증 근거→두 배포 환경→한계 순서로3분 시연하고 짝에게 개선점1개를 받는다. 실SNS 연결 미완료는 미확인으로 설명한다.
완성한 과제 체크 앱의 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가 만들었다”보다 문제와 검증 근거를 평가한다.
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·실패 검사·복구 담당자가 분명하다.
성공 기준은 공개 링크 하나가 아니라 재현 가능한 실행·권한·배포 근거다. 계정 삭제/보관기간/관리자 대시보드/원본 백업은 후속 기능으로 이슈화한다. 교육용 체크 통과는 전체 보안 감사 완료를 의미하지 않는다.
버튼은 현재 검색/필터와 무관하게 로그인한 본인 과제 전체를 새로 조회한다. 현재 화면 목록은 최대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 원본은 그대로 둔다.
// 사람의 스프레드시트 열람용입니다. 원본 복원/재가져오기 형식이 아닙니다.
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;
}
}
};
}
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()));
}
};
}
목차와 처음으로 ↑| 세션 | 조회 | 추가 | 수정/삭제 | CSV |
|---|---|---|---|---|
| 익명 | 권한거절 | 거절 | 거절 | UI/인증거절 |
| A본인 AAL1 | 0행 | 거절 | 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를 함께 유지한다.
목차와 처음으로 ↑| 환경 | 브랜치 | Vercel 범위 | DB | 앱 주소 |
|---|---|---|---|---|
| 로컬 | feature/csv-release | Development / .env.local | 수업 staging DB | http://127.0.0.1:5173/ |
| staging | staging | Preview (필요 시 branch override) | 별도 수업 staging DB | 실제 고정/현재 Preview URL |
| Production | main | Production | 별도 수업 production DB | 본인 운영 URL |
이 수업은 main의 Git 자동 배포를 사용한다. Preview 검증만으로 Production의 환경·메일·SNS가 검증되지는 않는다. Vercel에서 Preview를 promote하는 별도 기능도 Production 설정으로 다시 빌드될 수 있으므로 Preview URL을 그대로 운영 검증 결과로 적지 않는다. Vercel 배포는 Supabase SQL을 자동 적용하지 않는다.
목차와 처음으로 ↑# 과제 체크 앱 최종 릴리스 기록
검증 일시/확인자:
저장소 / 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/프로젝트 이름과 검사 시각이면 근거를 대조할 수 있다.
목차와 처음으로 ↑| 시간 | 내용 | 보여줄 근거 |
|---|---|---|
| 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 | 실제 대상·문제·웹/모바일 선택 이유 |
| 핵심 동작 | 25 | CRUD·검색·CSV 실제 시연 |
| 권한·보안 근거 | 25 | A/B·익명·AAL·실패검증/미확인 구분 |
| 배포·재현성 | 20 | 두 환경·PR·commit·README·복구 기록 |
| 발표·개선 | 15 | 3분 준수·비밀없는가상자료·다음 이슈 |
발표3분+피드백2분을6명 소그룹으로30분 운영한다. 발표용 MFA는 사전 완료하고 비밀 QR/OTP를 화면 공유하지 않는다. CSV도 본인 가상 과제만 포함한다. 개선점1개는 peer-feedback.md와 GitHub 이슈에 남긴다.
| 검사 | 행동 | 기대 결과 |
|---|---|---|
| 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거절 |
| 본인AAL2 | MFA후 CRUD/CSV | 본인만 성공·재조회유지 |
| 타인AAL2 | B가 A id조회/수정/삭제 | 0행 또는 거절·A원본불변 |
| 소유자위조 | B의user_id=A추가/변경 | 거절·DB불변 |
| 비밀 | tracked파일·diff·번들검사 | secret/service_role/SMTP/OAuth비밀없음 |
| staging | Preview URL·commit·DB·기능검사 | staging DB·로그인/MFA/CRUD/CSV/A-B확인 |
| main | Production URL·commit·DB·재검사 | production DB·로그인/MFA/CRUD/CSV/A-B확인 |
| 발표 | 3분 시연·동료 피드백·이슈 | 가상자료·근거/미확인분리·다음개선1개 |
내 과제 앱의 최종 검증을 실행 가능한 범위에서 진행하고 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/코드/토큰을 근거로 첨부하지 않는다.
목차와 처음으로 ↑| 증상 | 확인 | 조치 |
|---|---|---|
| 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 제공자 승인/동의 왕복, 실제 교실 스프레드시트 열람은 각 수업 계정에서 최종 확인해야 한다. 강의 사이트 게시와 학생 앱의 두 환경 릴리스는 별개다.